Newer
Older
mobile.raikyakun.app / node / src / app / actions / page.tsx
nutrition on 24 Jul 2024 12 KB first commit
'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>
	)
}