Update button re-enable timeout to 3 seconds, update thing history
This commit is contained in:
parent
6bc28be9e3
commit
5e54da8271
9 changed files with 1004 additions and 92 deletions
|
@ -197,6 +197,13 @@ const DeleteThing = id => {
|
|||
})
|
||||
}
|
||||
|
||||
const GetThingHistory = (id, offset) => {
|
||||
return Fetch(`${API_URL}/things/${id}/history?offset=${offset}`, {
|
||||
method: 'GET',
|
||||
headers: HEADERS(),
|
||||
})
|
||||
}
|
||||
|
||||
const CreateLongLiveToken = name => {
|
||||
return Fetch(`${API_URL}/users/tokens`, {
|
||||
method: 'POST',
|
||||
|
@ -236,6 +243,7 @@ export {
|
|||
GetCircleMemberRequests,
|
||||
GetLongLiveTokens,
|
||||
GetSubscriptionSession,
|
||||
GetThingHistory,
|
||||
GetThings,
|
||||
GetUserCircle,
|
||||
GetUserProfile,
|
||||
|
|
|
@ -506,7 +506,7 @@ const ChoreEdit = () => {
|
|||
</FormControl>
|
||||
)}
|
||||
</Box>
|
||||
{!['once', 'no_repeat'].includes(frequencyType) && (
|
||||
{!['once', 'no_repeat', 'trigger'].includes(frequencyType) && (
|
||||
<Box mt={2}>
|
||||
<Typography level='h4'>Scheduling Preferences: </Typography>
|
||||
<Typography level='h5'>
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
Card,
|
||||
Chip,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
|
@ -91,7 +90,7 @@ const ThingTriggerSection = ({
|
|||
<Typography level='h5'>
|
||||
Trigger a task when a thing state changes to a desired state
|
||||
</Typography>
|
||||
{things.length !== 0 && (
|
||||
{things?.length === 0 && (
|
||||
<Typography level='body-sm'>
|
||||
it's look like you don't have any things yet, create a thing to
|
||||
trigger a task when the state changes.
|
||||
|
|
|
@ -125,7 +125,7 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
|
|||
}
|
||||
})
|
||||
setIsDisabled(true)
|
||||
setTimeout(() => setIsDisabled(false), 5000) // Re-enable the button after 5 seconds
|
||||
setTimeout(() => setIsDisabled(false), 3000) // Re-enable the button after 5 seconds
|
||||
}
|
||||
const handleChangeDueDate = newDate => {
|
||||
if (activeUserId === null) {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Input,
|
||||
Modal,
|
||||
|
@ -14,8 +16,9 @@ import { useEffect, useState } from 'react'
|
|||
|
||||
function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
||||
const [name, setName] = useState(currentThing?.name || '')
|
||||
const [type, setType] = useState(currentThing?.type || 'numeric')
|
||||
const [type, setType] = useState(currentThing?.type || 'number')
|
||||
const [state, setState] = useState(currentThing?.state || '')
|
||||
const [errors, setErrors] = useState({})
|
||||
useEffect(() => {
|
||||
if (type === 'boolean') {
|
||||
if (state !== 'true' && state !== 'false') {
|
||||
|
@ -27,7 +30,31 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
|||
}
|
||||
}
|
||||
}, [type])
|
||||
|
||||
const isValid = () => {
|
||||
const newErrors = {}
|
||||
if (!name || name.trim() === '') {
|
||||
newErrors.name = 'Name is required'
|
||||
}
|
||||
|
||||
if (type === 'number' && isNaN(state)) {
|
||||
newErrors.state = 'State must be a number'
|
||||
}
|
||||
if (type === 'boolean' && !['true', 'false'].includes(state)) {
|
||||
newErrors.state = 'State must be true or false'
|
||||
}
|
||||
if ((type === 'text' && !state) || state.trim() === '') {
|
||||
newErrors.state = 'State is required'
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (!isValid()) {
|
||||
return
|
||||
}
|
||||
onSave({ name, type, id: currentThing?.id, state: state || null })
|
||||
onClose()
|
||||
}
|
||||
|
@ -36,64 +63,81 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
|||
<Modal open={isOpen} onClose={onClose}>
|
||||
<ModalDialog>
|
||||
{/* <ModalClose /> */}
|
||||
<Typography variant='h4'>P;lease add info</Typography>
|
||||
<FormLabel>Name</FormLabel>
|
||||
|
||||
<Textarea
|
||||
placeholder='Thing name'
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
sx={{ minWidth: 300 }}
|
||||
/>
|
||||
<FormLabel>Type</FormLabel>
|
||||
<Select value={type} sx={{ minWidth: 300 }}>
|
||||
{['text', 'number', 'boolean'].map(type => (
|
||||
<Option value={type} key={type} onClick={() => setType(type)}>
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{type === 'text' && (
|
||||
<>
|
||||
<FormLabel>Value</FormLabel>
|
||||
<Input
|
||||
placeholder='Thing value'
|
||||
value={state || ''}
|
||||
onChange={e => setState(e.target.value)}
|
||||
<Typography level='h4'>
|
||||
{currentThing?.id ? 'Edit' : 'Create'} Thing
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<FormLabel>
|
||||
Name
|
||||
<Textarea
|
||||
placeholder='Thing name'
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
sx={{ minWidth: 300 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === 'number' && (
|
||||
<>
|
||||
<FormLabel>Value</FormLabel>
|
||||
<Input
|
||||
placeholder='Thing value'
|
||||
type='number'
|
||||
value={state || ''}
|
||||
onChange={e => {
|
||||
setState(e.target.value)
|
||||
}}
|
||||
sx={{ minWidth: 300 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === 'boolean' && (
|
||||
<>
|
||||
<FormLabel>Value</FormLabel>
|
||||
<Select sx={{ minWidth: 300 }} value={state}>
|
||||
{['true', 'false'].map(value => (
|
||||
<Option
|
||||
value={value}
|
||||
key={value}
|
||||
onClick={() => setState(value)}
|
||||
>
|
||||
{value.charAt(0).toUpperCase() + value.slice(1)}
|
||||
</FormLabel>
|
||||
<FormHelperText color='danger'>{errors.name}</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>
|
||||
Type
|
||||
<Select value={type} sx={{ minWidth: 300 }}>
|
||||
{['text', 'number', 'boolean'].map(type => (
|
||||
<Option value={type} key={type} onClick={() => setType(type)}>
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
</FormLabel>
|
||||
<FormHelperText color='danger'>{errors.type}</FormHelperText>
|
||||
</FormControl>
|
||||
{type === 'text' && (
|
||||
<FormControl>
|
||||
<FormLabel>
|
||||
Value
|
||||
<Input
|
||||
placeholder='Thing value'
|
||||
value={state || ''}
|
||||
onChange={e => setState(e.target.value)}
|
||||
sx={{ minWidth: 300 }}
|
||||
/>
|
||||
</FormLabel>
|
||||
<FormHelperText color='danger'>{errors.state}</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
{type === 'number' && (
|
||||
<FormControl>
|
||||
<FormLabel>
|
||||
Value
|
||||
<Input
|
||||
placeholder='Thing value'
|
||||
type='number'
|
||||
value={state || ''}
|
||||
onChange={e => {
|
||||
setState(e.target.value)
|
||||
}}
|
||||
sx={{ minWidth: 300 }}
|
||||
/>
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
)}
|
||||
{type === 'boolean' && (
|
||||
<FormControl>
|
||||
<FormLabel>
|
||||
Value
|
||||
<Select sx={{ minWidth: 300 }} value={state}>
|
||||
{['true', 'false'].map(value => (
|
||||
<Option
|
||||
value={value}
|
||||
key={value}
|
||||
onClick={() => setState(value)}
|
||||
>
|
||||
{value.charAt(0).toUpperCase() + value.slice(1)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||
|
@ -108,5 +152,4 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
|
|||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateThingModal
|
||||
|
|
|
@ -1,11 +1,171 @@
|
|||
import { Container, Typography } from '@mui/joy'
|
||||
import { EventBusy } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Container,
|
||||
List,
|
||||
ListDivider,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { GetThingHistory } from '../../utils/Fetcher'
|
||||
|
||||
const ThingsHistory = () => {
|
||||
const { id } = useParams()
|
||||
const [thingsHistory, setThingsHistory] = useState([])
|
||||
const [noMoreHistory, setNoMoreHistory] = useState(false)
|
||||
const [errLoading, setErrLoading] = useState(false)
|
||||
useEffect(() => {
|
||||
GetThingHistory(id, 0, 10).then(resp => {
|
||||
if (resp.ok) {
|
||||
resp.json().then(data => {
|
||||
setThingsHistory(data.res)
|
||||
if (data.res.length < 10) {
|
||||
setNoMoreHistory(true)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setErrLoading(true)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleLoadMore = () => {
|
||||
GetThingHistory(id, thingsHistory.length).then(resp => {
|
||||
if (resp.ok) {
|
||||
resp.json().then(data => {
|
||||
setThingsHistory([...thingsHistory, ...data.res])
|
||||
if (data.res.length < 10) {
|
||||
setNoMoreHistory(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const formatTimeDifference = (startDate, endDate) => {
|
||||
const diffInMinutes = moment(startDate).diff(endDate, 'minutes')
|
||||
let timeValue = diffInMinutes
|
||||
let unit = 'minute'
|
||||
|
||||
if (diffInMinutes >= 60) {
|
||||
const diffInHours = moment(startDate).diff(endDate, 'hours')
|
||||
timeValue = diffInHours
|
||||
unit = 'hour'
|
||||
|
||||
if (diffInHours >= 24) {
|
||||
const diffInDays = moment(startDate).diff(endDate, 'days')
|
||||
timeValue = diffInDays
|
||||
unit = 'day'
|
||||
}
|
||||
}
|
||||
|
||||
return `${timeValue} ${unit}${timeValue !== 1 ? 's' : ''}`
|
||||
}
|
||||
if (errLoading || !thingsHistory) {
|
||||
return (
|
||||
<Container
|
||||
maxWidth='md'
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
// make sure the content is centered vertically:
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
height: '50vh',
|
||||
}}
|
||||
>
|
||||
<EventBusy
|
||||
sx={{
|
||||
fontSize: '6rem',
|
||||
// color: 'text.disabled',
|
||||
mb: 1,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography level='h3' gutterBottom>
|
||||
No history found
|
||||
</Typography>
|
||||
<Typography level='body1'>
|
||||
It's look like there is no history for this thing yet.
|
||||
</Typography>
|
||||
<Button variant='soft' sx={{ mt: 2 }}>
|
||||
<Link to='/things'>Go back to things</Link>
|
||||
</Button>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth='md'>
|
||||
<Typography level='h3' mb={1.5}>
|
||||
Summary:
|
||||
History:
|
||||
</Typography>
|
||||
<List sx={{ p: 0 }}>
|
||||
{thingsHistory.map((history, index) => (
|
||||
<>
|
||||
<ListItem sx={{ gap: 1.5, alignItems: 'flex-start' }}>
|
||||
<ListItemContent sx={{ my: 0 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography level='body1' sx={{ fontWeight: 'md' }}>
|
||||
{moment(history.updatedAt).format(
|
||||
'ddd MM/DD/yyyy HH:mm:ss',
|
||||
)}
|
||||
</Typography>
|
||||
<Chip>{history.state === '1' ? 'Active' : 'Inactive'}</Chip>
|
||||
</Box>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
{index < thingsHistory.length - 1 && (
|
||||
<>
|
||||
<ListDivider component='li'>
|
||||
{/* time between two completion: */}
|
||||
{index < thingsHistory.length - 1 &&
|
||||
thingsHistory[index + 1].createdAt && (
|
||||
<Typography level='body3' color='text.tertiary'>
|
||||
{formatTimeDifference(
|
||||
history.createdAt,
|
||||
thingsHistory[index + 1].createdAt,
|
||||
)}{' '}
|
||||
before
|
||||
</Typography>
|
||||
)}
|
||||
</ListDivider>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</List>
|
||||
{/* Load more Button */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant='plain'
|
||||
fullWidth
|
||||
color='primary'
|
||||
onClick={handleLoadMore}
|
||||
disabled={noMoreHistory}
|
||||
>
|
||||
{noMoreHistory ? 'No more history' : 'Load more'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,12 +12,15 @@ import {
|
|||
Box,
|
||||
Card,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Container,
|
||||
Grid,
|
||||
IconButton,
|
||||
Snackbar,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
CreateThing,
|
||||
DeleteThing,
|
||||
|
@ -27,13 +30,14 @@ import {
|
|||
} from '../../utils/Fetcher'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import CreateThingModal from '../Modals/Inputs/CreateThingModal'
|
||||
|
||||
const ThingCard = ({
|
||||
thing,
|
||||
onEditClick,
|
||||
onStateChangeRequest,
|
||||
onDeleteClick,
|
||||
}) => {
|
||||
const [isDisabled, setIsDisabled] = useState(false)
|
||||
const Navigate = useNavigate()
|
||||
const getThingIcon = type => {
|
||||
if (type === 'text') {
|
||||
return <Flip />
|
||||
|
@ -49,6 +53,15 @@ const ThingCard = ({
|
|||
return <ToggleOff />
|
||||
}
|
||||
}
|
||||
|
||||
const handleRequestChange = thing => {
|
||||
setIsDisabled(true)
|
||||
onStateChangeRequest(thing)
|
||||
setTimeout(() => {
|
||||
setIsDisabled(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
variant='outlined'
|
||||
|
@ -71,6 +84,9 @@ const ThingCard = ({
|
|||
flexDirection: 'row',
|
||||
gap: 1,
|
||||
}}
|
||||
onClick={() => {
|
||||
Navigate(`/things/${thing?.id}`)
|
||||
}}
|
||||
>
|
||||
<Typography level='title-lg' component='h2'>
|
||||
{thing?.name}
|
||||
|
@ -91,21 +107,39 @@ const ThingCard = ({
|
|||
<Grid item xs={3}>
|
||||
<Box display='flex' justifyContent='flex-end' alignItems='flex-end'>
|
||||
{/* <ButtonGroup> */}
|
||||
<IconButton
|
||||
variant='solid'
|
||||
color='success'
|
||||
onClick={() => {
|
||||
onStateChangeRequest(thing)
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: '50%',
|
||||
width: 50,
|
||||
height: 50,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{getThingIcon(thing?.type)}
|
||||
</IconButton>
|
||||
<div className='relative grid place-items-center'>
|
||||
<IconButton
|
||||
variant='solid'
|
||||
color='success'
|
||||
onClick={() => {
|
||||
handleRequestChange(thing)
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: '50%',
|
||||
width: 50,
|
||||
minWidth: 50,
|
||||
height: 50,
|
||||
zIndex: 1,
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{getThingIcon(thing?.type)}
|
||||
</IconButton>
|
||||
{isDisabled && (
|
||||
<CircularProgress
|
||||
variant='solid'
|
||||
color='success'
|
||||
size='md'
|
||||
sx={{
|
||||
color: 'success.main',
|
||||
position: 'absolute',
|
||||
'--CircularProgress-size': '55px',
|
||||
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<IconButton
|
||||
// sx={{ width: 15 }}
|
||||
variant='soft'
|
||||
|
@ -154,6 +188,10 @@ const ThingsView = () => {
|
|||
const [isShowCreateThingModal, setIsShowCreateThingModal] = useState(false)
|
||||
const [createModalThing, setCreateModalThing] = useState(null)
|
||||
const [confirmModelConfig, setConfirmModelConfig] = useState({})
|
||||
|
||||
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
|
||||
const [snackBarMessage, setSnackBarMessage] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
// fetch things
|
||||
GetThings().then(result => {
|
||||
|
@ -184,6 +222,8 @@ const ThingsView = () => {
|
|||
}
|
||||
})
|
||||
})
|
||||
setSnackBarMessage('Thing saved successfully')
|
||||
setIsSnackbarOpen(true)
|
||||
}
|
||||
const handleEditClick = thing => {
|
||||
setCreateModalThing(thing)
|
||||
|
@ -240,6 +280,8 @@ const ThingsView = () => {
|
|||
})
|
||||
})
|
||||
}
|
||||
setSnackBarMessage('Thing state updated successfully')
|
||||
setIsSnackbarOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -317,6 +359,19 @@ const ThingsView = () => {
|
|||
)}
|
||||
<ConfirmationModal config={confirmModelConfig} />
|
||||
</Box>
|
||||
<Snackbar
|
||||
open={isSnackbarOpen}
|
||||
onClose={() => {
|
||||
setIsSnackbarOpen(false)
|
||||
}}
|
||||
autoHideDuration={3000}
|
||||
variant='soft'
|
||||
color='success'
|
||||
size='lg'
|
||||
invertedColors
|
||||
>
|
||||
<Typography level='title-md'>{snackBarMessage}</Typography>
|
||||
</Snackbar>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue