'use client'
import React, { useMemo, useState, useEffect, useRef } from 'react'
import { Container, Box, Grid, Alert, ButtonGroup, RadioGroup, Radio, FormControlLabel, Button, TextField, Typography, InputAdornment, IconButton, FormControl, InputLabel, OutlinedInput, FormHelperText, CircularProgress, Select, MenuItem,
AppBar, Toolbar
} from '@mui/material'
import {ArrowBackIosNew as ArrowBackIosNewIcon} from '@mui/icons-material'
import { CircularProgressProps } from '@mui/material/CircularProgress'
import { useSearchParams } from 'next/navigation'
import { User } from '@/types'
import axios, { AxiosError } from 'axios'
import { useRouter } from 'next/navigation'
import './page.css'
import { useIndexedDB } from '@/hooks'
interface Notification {
title: string
messsage: string
callId: string
visitor: string
dst: User
createdAt: number
}
/* 応答結果と来訪者へのメッセージ */
interface ActionReply {
callId: string
userId: string
result: string
optionMessage: string
}
/* 代替対応者へメッセージを送る */
interface ActionContact {
callId: string
message: string
}
const TIMEOUT = 59
export default function Actions(){
const searchParams = useSearchParams()
const action = searchParams.get('action')
const router = useRouter()
const [radioValue, setRadioValue] = useState<string>('default')
const [selectedTime, setSelectedTime] = useState<string | null>('5分')
const [customMessage, setCustomMessage] = useState('')
const [count, setCount] = useState(TIMEOUT)
const [users, setUsers] = useState<User[]>([])
const [userId, setUserId] = useState('')
const [messages, setMessages] = useState<string[]>([])
const [templateMessage, setTemplateMessage] = useState('')
const [isAccept, setIsAccept] = useState<null | boolean>(action === 'accept' ? true : action === 'reject' ? false : null)
const [accessKeyError, setAccessKeyError] = useState<null | string>(null)
const [submitResult, setSubmitResult] = useState<'' | 'pending' | 'ok' | 'timeout' | 'error'>('')
const [isLoading, setIsLoading] = useState(true)
const reqIdRef = useRef(0)
const timeout = useRef(TIMEOUT)
const { latestCall, updateResponder } = useIndexedDB()
useEffect(() => {
setIsLoading(true)
const messagesJSON = localStorage.getItem('messages')
if(messagesJSON) {
const messages = JSON.parse(messagesJSON) as string[]
setMessages(messages)
if(messages.length > 0) setTemplateMessage(messages[0])
}
const accessKey = localStorage.getItem('AccessKey')
if(!accessKey) {
setAccessKeyError('アクセスキーがありません')
return
}
axios.get<{users: User[], hash: null | string}>('/api/mobile/' + accessKey)
.then(({data:{users}}) => {
setUsers(users)
if(users.length > 0) {
const userId = users[0].id
setUserId(userId)
}
setIsLoading(false)
})
.catch(err => {
setUsers([])
console.error(err)
if (axios.isAxiosError(err)) {
const serverError = err as AxiosError<{error: string}>
if (serverError && serverError.response) setAccessKeyError(serverError.response.data.error)
else setAccessKeyError('接続に失敗しました')
} else setAccessKeyError('不明なエラーが発生しました')
setIsLoading(false)
})
}, [])
const notification = useMemo(()=>{
const dataJSON = searchParams.get('data')
if(!dataJSON) return null
const {createdAt, ...theOthers} = JSON.parse(dataJSON)
return {...theOthers, createdAt: new Date(createdAt).getTime()/1000 } as Notification
}, [searchParams])
const message = useMemo(()=>{
switch(radioValue){
case 'time': return `${selectedTime}ほどお待ちください`
case 'custom': return customMessage
case 'template': return templateMessage
default: return ''
}
}, [radioValue, selectedTime, customMessage, templateMessage])
useEffect(()=>{
if(!notification) return
console.log('notification', notification)
// タイムアウトの設定
const { createdAt } = notification
timeout.current -= Date.now()/1000 - createdAt
let last = performance.now()
const draw = () => {
const now = performance.now()
const diff = (now-last)/1000
last = now
if(timeout.current > 0){
timeout.current -= diff
setCount(timeout.current)
reqIdRef.current = requestAnimationFrame(draw)
} else {
// タイムアウトした場合
setCount(0)
setIsAccept(false)
setRadioValue('default')
setSubmitResult('timeout')
}
}
draw()
return () => {
console.log("Countdownキャンセル")
cancelAnimationFrame(reqIdRef.current)
}
}, [notification])
const handleSelect = (value: string) => setSelectedTime(value)
const handleSubmit = (isAccept: boolean) => {
if(!notification) return
setIsAccept(isAccept)
setSubmitResult('pending')
const accessKey = localStorage.getItem('AccessKey')
if(!accessKey) {
window.alert('アクセスキーが無いため失敗しました')
return
}
const { callId } = notification
const payload: ActionReply = {callId, userId, result: isAccept ? 'accept':'reject', optionMessage: message}
axios.post(`/api/mobile/${accessKey}/actions/reply`, payload)
.then(()=>{
setSubmitResult('ok')
const responder = users.find(user => user.id == userId)
if(responder) updateResponder(callId, `[${responder.group}]${responder.name}`)
})
.catch(err => {
setSubmitResult('error')
console.error('送信失敗しました', err)
})
console.log('res')
}
const disabled = isLoading || isAccept != null
const cancel = () => {
if(notification && latestCall && !('responder' in latestCall) ) {
const { callId } = notification
updateResponder(callId, null)
}
router.replace('/')
}
return (<>
<AppBar position="static" color="inherit">
<Toolbar>
<Button size="large" startIcon={<ArrowBackIosNewIcon />} sx={{fontWeight: 'bold'}} onClick={cancel}>戻る</Button>
</Toolbar>
</AppBar>
<Container>
<Box sx={{textAlign: 'center', marginTop: 5}}>
<Typography sx={{fontSize: '7vw'}}>「{notification?.visitor == ''?'(未入力)':notification?.visitor}」様</Typography>
<Grid container spacing={2}>
<Grid item xs={10}>
<Typography sx={{fontSize: '4.6vw'}}>訪問先: [{notification?.dst.group}] {notification?.dst.name}</Typography>
</Grid>
<Grid item xs={2} >
{submitResult === '' && <CircularProgressWithLabel value={count} sx={{display: 'flex', alignItems: 'center', justifyContent: 'center'}} />}
{submitResult === 'pending' && <CircularProgress sx={{display: 'flex', alignItems: 'center', justifyContent: 'center'}} size="10vw" />}
</Grid>
<Grid item xs={12} md={12}>
<RadioGroup sx={{width: '100%'}} defaultValue="default" value={radioValue} onChange={e => setRadioValue(e.target.value)}>
<FormControlLabel sx={{m: 1}} value="default" control={<Radio />} label="しばらくお待ち下さい (デフォルト)" disabled={disabled} />
<FormControlLabel sx={{m: 1}} value="time" control={<Radio />} disabled={disabled} label={<>
<ButtonGroup onFocus={()=>setRadioValue('time')}>
<Button size="small" variant={selectedTime==='5分'?'contained':'outlined'} onClick={() => handleSelect('5分')} disabled={disabled}>5分</Button>
<Button size="small" variant={selectedTime==='10分'?'contained':'outlined'} onClick={() => handleSelect('10分')} disabled={disabled}>10分</Button>
<Button size="small" variant={selectedTime==='15分'?'contained':'outlined'} onClick={() => handleSelect('15分')} disabled={disabled}>15分</Button>
<Button size="small" variant={selectedTime==='30分'?'contained':'outlined'} onClick={() => handleSelect('30分')} disabled={disabled}>30分</Button>
<Button size="small" variant={selectedTime==='1時間'?'contained':'outlined'} onClick={() => handleSelect('1時間')} disabled={disabled}>1時間</Button>
</ButtonGroup><br />
<Box sx={{textAlign: 'left'}}>ほどお待ちください</Box>
</>} />
<FormControlLabel sx={{m: 1, width: '100%'}} value="custom" control={<Radio />} disabled={disabled} label={
<TextField sx={{width: 'calc(80vw - 1rem)'}} value={customMessage} onChange={e=>setCustomMessage(e.target.value)} onFocus={()=>setRadioValue('custom')} label="カスタムメッセージ" name="message" disabled={disabled} />
} />
{messages.length != 0 && <FormControlLabel sx={{m: 1, width: '100%'}} value="template" control={<Radio />} disabled={disabled} label={
messages.length == 1 ? messages[0]
: <Select value={templateMessage} onChange={e => setTemplateMessage(e.target.value)} onFocus={()=>setRadioValue('template')} disabled={disabled}>
{messages.map((message, index) => <MenuItem key={index} value={message}>{message}</MenuItem>)}
</Select>
} />}
</RadioGroup>
<FormControl sx={{ m: 1, minWidth: 120 }}>
{users.length == 1 &&
<div>
<div style={{color: 'gray'}}>対応者名義</div>
<div style={{border: '1px solid lightgray', borderRadius: 5, padding: 7, fontSize: '130%'}}>[{users[0].group}] {users[0].name}</div>
</div>
}
{users.length > 1 &&
<>
<Select value={userId} onChange={e => setUserId(e.target.value)} displayEmpty inputProps={{ 'aria-label': 'Without label' }} disabled={disabled}>
{users?.map((user, index) => <MenuItem key={user.id} value={user.id}>[{user.group}] {user.name}</MenuItem>)}
</Select>
<FormHelperText>対応者の名義を選択してください</FormHelperText>
</>
}
</FormControl>
{(submitResult === '' || submitResult === 'pending') && <div style={{visibility: 'hidden', border: '1px solid', padding: 7, marginTop: 30, background: 'azure'}}>制限時間内に選択してください</div>}
{submitResult === 'ok' && <div style={{border: '1px solid dodgerblue', width: '80%', margin: 'auto', borderRadius: 5, color: 'dodgerblue', padding: 7, marginTop: 30, background: 'azure'}}>送信成功しました</div>}
{submitResult === 'error' && <div style={{border: '1px solid crimson', width: '80%', margin: 'auto', borderRadius: 5, color: 'crimson', padding: 7, marginTop: 30, background: 'mistyrose'}}>送信失敗しました</div>}
{submitResult === 'timeout' && <div style={{border: '1px solid darkorange', width: '80%', margin: 'auto', borderRadius: 5, color: 'darkorange', padding: 7, marginTop: 30, background: 'oldlace'}}>有効期限が過ぎました</div>}
{accessKeyError && <div style={{border: '1px solid crimson', width: '80%', margin: '20px auto', borderRadius: 5, color: 'crimson', padding: 7, marginTop: 30, background: 'mistyrose'}}>{accessKeyError}</div>}
{isLoading && <div style={{margin: '20px auto'}}>読み込み中…</div>}
{!isLoading && <div className="action" style={{marginTop: 30}}>
<span onClick={()=>isAccept === null && handleSubmit(true)} className={isAccept != null?isAccept == true?'accept selected':'accept disabled':'accept enable'}><img src="/images/icon_checkbox_accept.png" />対応可能</span>
<span onClick={()=>isAccept === null && handleSubmit(false)} className={isAccept != null?isAccept == false?'reject selected':'reject disabled':'reject enable'}><img src="/images/icon_checkbox_reject.png" />対応不可</span>
</div>}
</Grid>
</Grid>
</Box>
</Container>
</>)
}
/*
<Typography>代わりに「◯◯◯」が対応します</Typography>
<TextField label="対応者への連絡事項" name="message" />
<FormHelperText>代理対応者に連絡事項を伝えられます</FormHelperText>
*/
function CircularProgressWithLabel(
props: CircularProgressProps & { value: number },
) {
return (
<Box {...props}>
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<CircularProgress sx={{color: props.value > 10 ? '#1976d2':'#d32f2f'}} variant="determinate" size="10vw" value={Math.floor(props.value/TIMEOUT*100)} />
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%'
}}
>
<Typography
variant="caption"
component="div"
color="text.secondary"
sx={{fontSize: '5vw', color: props.value > 10 ? '#1976d2':'#d32f2f'}}
>
{Math.floor(props.value)}
</Typography>
</Box>
</Box>
</Box>
)
}