update jamespong14205,

This commit is contained in:
louiscklaw
2025-02-01 02:02:25 +08:00
parent 8bf2589af5
commit c3a16177eb
90 changed files with 9071 additions and 6 deletions

View File

@@ -2,14 +2,9 @@
set -ex set -ex
git config --global http.version HTTP/1.1
git config --global lfs.allowincompletepush true
git config --global lfs.locksverify true
git config --global http.postBuffer 5368709120
git add . git add .
git commit -m 'update,' git commit -m"update jamespong14205,"
git push git push

20
meta.md Normal file
View File

@@ -0,0 +1,20 @@
---
tags: js, app, db, nextjs, login, registration
---
# apps
### ver 1:
https://youtu.be/KOTr7nnr6uk
balance:
| item | amount |
| ------------------- | ---------- |
| quote | + HKD 2000 |
| deposit received | - HKD 500 |
| outstanding | HKD 1500 |
| | |
| vo (search page) | + HKD 350 |
| current outstanding | HKD 1850 |

1
task1/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
**/volumes/*

33
task1/README.md Normal file
View File

@@ -0,0 +1,33 @@
# README
### known background requirement
1. align `.wslconfig` for below parameters
```
[wsl2]
memory=4G
processors=4
swap=16G
```
### start development/demo server
```bash
# backup
$ node ./backup.js
# 1. start docker
$ ./dc_up.sh
# 2.
$ docker compose exec -it client bash
# 3. into client, spin up dev server
$ yarn
$ yarn demo
# 4. brows localhost -> http://localhost
```

BIN
task1/archive.zip (Stored with Git LFS) Normal file

Binary file not shown.

59
task1/backup.js Normal file
View File

@@ -0,0 +1,59 @@
const execSync = require('child_process').execSync;
const fs = require('fs');
const path = require('path');
// Function to get all subdirectories of a given directory
function getDirectories(srcPath, excludeDirs) {
return fs
.readdirSync(srcPath)
.filter(file => !excludeDirs.includes(file) && fs.lstatSync(path.join(srcPath, file)).isDirectory())
.map(name => path.join(srcPath, name));
}
// Get current working directory
const cwd = process.cwd();
// Path to app-head directory
const appHeadDir = path.join(cwd, 'project');
// Check if app-head exists
if (!fs.existsSync(appHeadDir)) {
console.error(`Error: ${appHeadDir} does not exist.`);
process.exit(1);
}
// Execute reset.bat scripts
try {
// execSync(`cmd /c "cd ${appHeadDir} && scripts\\reset.bat"`, { stdio: 'inherit' });
} catch (err) {
console.error(`Error executing reset.bat script: ${err.message}`);
process.exit(1);
}
// Define excluded directories
const excludedDirs = ['.next', 'node_modules', '.git', 'volumes'];
// Copy app-head directory and its contents to a new directory with an increasing number suffix
let maxNum = 0;
const directories = getDirectories(cwd, excludedDirs);
for (const dir of directories) {
const match = dir.match(/^.+draft(\d+)$/);
if (match) {
const num = parseInt(match[1], 10);
if (num > maxNum) {
maxNum = num;
}
}
}
var zerofilled = ('0000' + (maxNum + 1)).slice(-4);
const targetDir = path.join(cwd, `draft${zerofilled}`);
fs.mkdirSync(targetDir);
// Copy app-head directory and its contents to targetDir, excluding specified directories
fs.cpSync(appHeadDir, targetDir, {
filter: src => !excludedDirs.includes(path.basename(src)),
recursive: true,
});
console.log(`Successfully copied ${appHeadDir} to ${targetDir}.`);

View File

@@ -0,0 +1,21 @@
const fs = require('fs');
const path = require('path');
function removeNodeModules(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === 'node_modules') {
fs.rmSync(fullPath, { recursive: true });
} else {
removeNodeModules(fullPath);
}
}
}
}
removeNodeModules('.');
console.log("All 'node_modules' directories have been removed.");

Binary file not shown.

View File

@@ -0,0 +1,5 @@
Q: 同埋遲啲係咪會做埋 Admin 可以 modify 啲 case🫡 delete modify 同 save 就 OK
A: 你比一比 D 掣個擺位我我放番佢地出黎 : )
Q: 如果佢 Admin login 嗰度入錯咗 Account 係咪都會 show
A: 係 : )

View File

@@ -0,0 +1,29 @@
# README
### known background requirement
1. align `.wslconfig` for below parameters
```
[wsl2]
memory=4G
processors=4
swap=16G
```
### start development/demo server
```bash
# 1. start docker
$ ./dc_up.sh
# 2.
$ docker compose exec -it client bash
# 3. into client, spin up dev server
$ yarn demo
# 4. brows localhost -> http://localhost
```

BIN
task1/project/002_notes/design/admin_case_list.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
task1/project/002_notes/design/admin_edit.jpg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
task1/project/002_notes/design/admin_home.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
task1/project/002_notes/design/admin_login.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
task1/project/002_notes/design/client_queue.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
task1/project/002_notes/design/client_register.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,39 @@
# Digest
### stack
- app
- db
### Registration form page (1page + 2 logic)
![alt text](register.png)
(Please add required fields checking for user didn't input data)
- If user select more than three option will pass to semi-urgent waiting list,
- If user select advice include (nausea and vomiting) also pass to semi-urgent waiting list,
- otherwise > non - urgent waiting list)
### Queue page (1 page + 2 logic)
![alt text](queue.png)
- DIsplay user waiting number
- waiting list type (semi-urgent /non-urgent)
### admin login page (1 page + 1 logic)
![alt text](admin_login.png)
### Administrator page (1 page + 2 logic)
![alt text](admin_home.png)
### case list view (2 pages + 2 logic)
![alt text](admin_case_list.png)
- case can be modify
- save
- deleted

BIN
task1/project/002_notes/design/search.jpg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,73 @@
module.exports = {
env: {
browser: true,
es2021: true,
jest: true,
},
extends: ['plugin:react/recommended', 'airbnb', 'prettier'],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: 'module',
},
plugins: ['react', 'prettier'],
rules: {
'react/react-in-jsx-scope': 'off',
'import/no-duplicates': 'error',
'import/no-unresolved': 'error',
'import/named': 'error',
'prettier/prettier': 'error',
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
'react/state-in-constructor': 'off',
'react/prop-types': 'off',
'react/no-access-state-in-setstate': 'error',
'react/no-danger': 'error',
'react/no-did-mount-set-state': 'error',
'react/no-did-update-set-state': 'error',
'react/no-will-update-set-state': 'error',
'react/no-redundant-should-component-update': 'error',
'react/no-this-in-sfc': 'error',
'react/no-typos': 'error',
'react/no-unused-state': 'error',
'react/jsx-no-bind': 'error',
'no-useless-call': 'error',
'no-useless-computed-key': 'error',
'no-useless-concat': 'error',
'no-useless-constructor': 'error',
'no-useless-rename': 'error',
'no-useless-return': 'error',
'react/jsx-props-no-spreading': 'off',
// overriding recommended rules
'no-constant-condition': ['error', { checkLoops: false }],
'no-console': ['error', { allow: ['log', 'warn', 'error'] }],
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-underscore-dangle': ['error'],
//
camelcase: 'off',
'no-alert': 'off',
},
settings: {
'import/resolver': {
node: {
paths: ['.'],
},
alias: {
map: [
['@/public', './public'],
['@/config', './config'],
// Add more here
],
extensions: ['.js', '.jsx'],
},
},
},
ignorePatterns: ['*_*', '*test*', '**/*debug*', '**/*copy*', '*helloworld*'],
};

39
task1/project/003_src/client/.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
**/*copy*
**/*.del
**/*.log
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel

View File

@@ -0,0 +1,8 @@
command_exists () {
command -v "$1" >/dev/null 2>&1
}
# Workaround for Windows 10, Git Bash and Yarn
if command_exists winpty && test -t 1; then
exec < /dev/tty
fi

View File

@@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
yarn lint-staged

View File

@@ -0,0 +1,18 @@
node_modules
.next
.DS_Store
static
.vercel
public/
.github/
.babelrc
README.md
# ignore deployment files
/.now
/.serverless
/.serverless_nextjs
/.vercel
.vercel
.now
.env

View File

@@ -0,0 +1,18 @@
module.exports = {
arrowParens: 'avoid',
bracketSpacing: true,
htmlWhitespaceSensitivity: 'css',
insertPragma: false,
bracketSameLine: false,
jsxSingleQuote: true,
printWidth: 120,
proseWrap: 'preserve',
quoteProps: 'as-needed',
requirePragma: false,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'all',
useTabs: false,
plugins: [require.resolve('prettier-plugin-organize-imports')],
};

View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Othneil Drew
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,31 @@
### endpoints
```bash
http://localhost/api/patient_queue/list
http://localhost/api/patient_queue/non_urgent_case
http://localhost/api/patient_queue/semi_urgent_case
```
### start demo
```bash
# start docker desktop in host
# inside wsl
# 1. start docker
$ ./dc_up.sh
# 2. get into container
$ docker compose exec -it client bash
# 3. start demo
$ ./demo.sh
# 4. browse http://localhost after complete
```
### test ac
username: admin
password: nimda

View File

@@ -0,0 +1,66 @@
import Box from '@mui/material/Box';
const row = {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
};
const labelColumn = {
fontWeight: 'bold',
width: '25%',
};
const valueColumn = {
width: '75%',
};
const cardTitle = {
fontWeight: 'bold',
fontSize: '1.2rem',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
};
export default function AdminQueueItemCard({ queue_data }) {
return (
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
margin: '1rem 0',
}}
>
<Box
sx={{
width: '90vw',
border: '1px solid gray',
borderRadius: '1rem',
display: 'flex',
flexDirection: 'column',
padding: '1rem',
}}
>
<Box sx={cardTitle}>{`non-urgent-case ${queue_data.id}`}</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', padding: '1rem 1rem' }}>
<Box sx={row}>
<Box sx={labelColumn}>Name</Box>
<Box sx={valueColumn}>{queue_data.name || ''}</Box>
</Box>
<Box sx={row}>
<Box sx={labelColumn}>HKID</Box>
<Box sx={valueColumn}>{queue_data.hkid || '-'}</Box>
</Box>
<Box sx={row}>
<Box sx={labelColumn}>Mobile</Box>
<Box sx={valueColumn}>{queue_data.mobile || '-'}</Box>
</Box>
</Box>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,15 @@
import { Box } from '@mui/material';
export default () => (
<Box
sx={{
width: '100%',
height: '90vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
loading...
</Box>
);

View File

@@ -0,0 +1,122 @@
import { Box, Button } from '@mui/material';
import { useRouter } from 'next/dist/client/router';
import { useState } from 'react';
const row = {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
};
const labelColumn = {
fontWeight: 'bold',
width: '25%',
};
const valueColumn = {
width: '75%',
};
const cardTitle = {
fontWeight: 'bold',
fontSize: '1.2rem',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
};
export default ({ queue_data, show_edit_delete = true }) => {
const router = useRouter();
const [changing_page, setChangingPage] = useState(false);
function handleDeleteItemClick({ queue_type, id }) {
setChangingPage(true);
fetch(`/api/patient_queue/delete_queue_item?queue_type=${queue_type}&id=${id}`, {
method: 'GET',
})
.then(response => response.json())
.then(data => {
if (data.status === 'OK') {
console.log('item deleted');
alert('item deleted');
router.reload();
} else {
console.error(data.message);
}
})
.catch(err => {
console.error(err);
alert('error during removing queue');
});
}
function handleEditItemClick({ id }) {
setChangingPage(true);
router.push(`/NonUrgentCaseEdit/${id}`);
}
return (
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
margin: '1rem 0',
}}
>
<Box
sx={{
width: '90vw',
border: '1px solid gray',
borderRadius: '1rem',
display: 'flex',
flexDirection: 'column',
padding: '1rem',
}}
>
<Box sx={cardTitle}>{`non-urgent-case ${queue_data.id}`}</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', padding: '0.15rem 0.15rem' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: '0.5rem', marginTop: '1rem' }}>
<Box sx={{ flexGrow: 1, fontSize: '0.8rem' }}>
<Box sx={row}>
<Box sx={labelColumn}>Name</Box>
<Box sx={valueColumn}>{queue_data.name || ''}</Box>
</Box>
<Box sx={row}>
<Box sx={labelColumn}>HKID</Box>
<Box sx={valueColumn}>{queue_data.hkid || '-'}</Box>
</Box>
<Box sx={row}>
<Box sx={labelColumn}>Mobile</Box>
<Box sx={valueColumn}>{queue_data.mobile || '-'}</Box>
</Box>
</Box>
<Box sx={{ display: show_edit_delete ? 'flex' : 'none', flexDirection: 'column', gap: '1rem' }}>
<Button
disabled={changing_page}
size='small'
variant='contained'
onClick={() => handleDeleteItemClick({ queue_type: 'non-urgent', id: queue_data.id })}
>
delete
</Button>
<Button
disabled={changing_page}
size='small'
variant='contained'
onClick={() => handleEditItemClick({ id: queue_data.id })}
>
edit
</Button>
{/* */}
</Box>
</Box>
</Box>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,118 @@
import { Box, Button } from '@mui/material';
import { useRouter } from 'next/dist/client/router';
import { useState } from 'react';
const row = {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
};
const labelColumn = {
fontWeight: 'bold',
width: '25%',
};
const valueColumn = {
width: '75%',
};
const cardTitle = {
fontWeight: 'bold',
fontSize: '1.2rem',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
};
export default ({ queue_data, show_edit_delete = true }) => {
const router = useRouter();
const [changing_page, setChangingPage] = useState(false);
function handleDeleteItemClick({ queue_type, id }) {
setChangingPage(true);
fetch(`/api/patient_queue/delete_queue_item?queue_type=${queue_type}&id=${id}`, {
method: 'GET',
})
.then(response => response.json())
.then(data => {
if (data.status === 'OK') {
console.log('item deleted');
router.reload();
} else {
console.error(data.message);
}
})
.catch(err => {
console.error(err);
});
}
function handleEditItemClick({ id }) {
setChangingPage(true);
router.push(`/SemiUrgentCaseEdit/${id}`);
}
return (
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
margin: '1rem 0',
}}
>
<Box
sx={{
width: '90vw',
border: '1px solid gray',
borderRadius: '1rem',
display: 'flex',
flexDirection: 'column',
padding: '1rem',
}}
>
<Box sx={cardTitle}>{`semi-urgent-case ${queue_data.id}`}</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', padding: '0.15rem 0.15rem' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: '0.5rem', marginTop: '1rem' }}>
<Box sx={{ flexGrow: 1, fontSize: '0.8rem' }}>
<Box sx={row}>
<Box sx={labelColumn}>Name</Box>
<Box sx={valueColumn}>{queue_data.name || ''}</Box>
</Box>
<Box sx={row}>
<Box sx={labelColumn}>HKID</Box>
<Box sx={valueColumn}>{queue_data.hkid || '-'}</Box>
</Box>
<Box sx={row}>
<Box sx={labelColumn}>Mobile</Box>
<Box sx={valueColumn}>{queue_data.mobile || '-'}</Box>
</Box>
</Box>
<Box sx={{ display: show_edit_delete ? 'flex' : 'none', flexDirection: 'column', gap: '1rem' }}>
<Button
disabled={changing_page}
size='small'
variant='contained'
onClick={() => handleDeleteItemClick({ queue_type: 'semi-urgent', id: queue_data.id })}
>
delete
</Button>
<Button
disabled={changing_page}
size='small'
variant='contained'
onClick={() => handleEditItemClick({ id: queue_data.id })}
>
edit
</Button>
</Box>
</Box>
</Box>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,15 @@
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
import { StylesProvider } from '@mui/styles';
import { ThemeProvider as StyledThemeProvider } from 'styled-components';
const MyThemeProvider = ({ theme, children }) => (
<StylesProvider injectFirst>
<CssBaseline />
<StyledThemeProvider theme={theme}>
<MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>
</StyledThemeProvider>
</StylesProvider>
);
export default MyThemeProvider;

View File

@@ -0,0 +1,37 @@
import { red } from '@mui/material/colors';
import { createTheme } from '@mui/material/styles';
// Create a theme instance.
const theme = createTheme({
palette: {
primary: {
main: '#556cd6',
},
secondary: {
main: '#19857b',
},
error: {
main: red.A400,
},
},
components: {
MuiTypography: {
defaultProps: {
variantMapping: {
h1: 'h2',
h2: 'h2',
h3: 'h2',
h4: 'h2',
h5: 'h2',
h6: 'h2',
subtitle1: 'h2',
subtitle2: 'h2',
body1: 'span',
body2: 'span',
},
},
},
},
});
export default theme;

View File

@@ -0,0 +1,47 @@
const { DataTypes } = require('sequelize');
const bcrypt = require('bcrypt');
const { sequelize } = require('./model');
const Auth = sequelize.define(
'Auths',
{
uid: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, unique: true },
username: { type: DataTypes.STRING, allowNull: false },
password: { type: DataTypes.STRING, allowNull: false },
session: { type: DataTypes.STRING, allowNull: false, defaultValue: '' },
role: { type: DataTypes.STRING, allowNull: false, defaultValue: 'customer' },
},
{ timestamps: false },
);
async function hashPassword(plainTextPassword) {
const saltRounds = 10;
return await bcrypt.hash(plainTextPassword, saltRounds);
}
(async () => {
try {
await sequelize.authenticate();
console.log('Connection has been established successfully.');
// create table
await Auth.drop();
await sequelize.sync();
let user_row;
let password;
user_row = await Auth.create({
username: 'admin',
password: 'nimda',
role: 'admin',
});
user_row = await Auth.create({ username: 'pat1', password: await hashPassword('1tap') });
user_row = await Auth.create({ username: 'pat2', password: await hashPassword('2tap') });
user_row = await Auth.create({ username: 'pat3', password: await hashPassword('3tap') });
await sequelize.close();
} catch (error) {
console.error('Unable to connect to the database:', error);
}
})();

View File

@@ -0,0 +1,15 @@
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize('app_db', 'db_user', 'db_user_pass', {
host: 'mysql',
port: 3306,
dialect: 'mysql',
});
const sequelize_config = new Sequelize('app_db', 'db_user', 'db_user_pass', {
host: 'mysql',
port: 3306,
dialect: 'mysql',
});
module.exports = { sequelize, sequelize_config };

View File

@@ -0,0 +1,48 @@
const { Sequelize, DataTypes } = require('sequelize');
// const { sequelize } = require('./model'); // abonded
const sequelize_config = require('../utils/sequelize_config');
const Item = sequelize_config.define(
'NonUrgentQueue',
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, unique: true },
name: { type: DataTypes.STRING, allowNull: false },
hkid: { type: DataTypes.STRING, allowNull: false },
mobile: { type: DataTypes.STRING, allowNull: false },
age: { type: DataTypes.INTEGER, allowNull: false },
description: { type: DataTypes.STRING, allowNull: false },
//
bruisesScratchesMinorBurns: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
chestPain: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
headache: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
myMuiCheck: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
nauseaAndVomiting: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
runnyOrStuffyNose: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
soreThroat: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
},
{ timestamps: false },
);
(async () => {
try {
await sequelize_config.authenticate();
console.log('Connection has been established successfully.');
await Item.drop();
await sequelize_config.sync();
for (let i = 0; i < 3; i++) {
await Item.create({
name: `non-urgent-patient ${i}`,
hkid: `A123456(${i})`,
mobile: `9123456${i}`,
age: i + 10,
description: `non-urgent queue ${i}`,
});
}
await sequelize_config.close();
} catch (error) {
console.error('Unable to connect to the database:', error);
}
})();

View File

@@ -0,0 +1,33 @@
const { Sequelize, DataTypes } = require('sequelize');
const { sequelize } = require('./model');
const Item = sequelize.define(
'PatientQueue',
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, unique: true },
description: { type: DataTypes.STRING, allowNull: false },
},
{ timestamps: false },
);
(async () => {
try {
await sequelize.authenticate();
console.log('Connection has been established successfully.');
await Item.drop();
await sequelize.sync();
for (let i = 0; i < 10; i++) {
await Item.create({
// get remainder of i divided by 3
// pid: (i % 25) + 1,
description: `test item ${i}`,
});
}
await sequelize.close();
} catch (error) {
console.error('Unable to connect to the database:', error);
}
})();

View File

@@ -0,0 +1,48 @@
const { Sequelize, DataTypes } = require('sequelize');
// const { sequelize } = require('./model'); //abonded
const sequelize_config = require('../utils/sequelize_config');
const Item = sequelize_config.define(
'SemiUrgentQueue',
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, unique: true },
name: { type: DataTypes.STRING, allowNull: false },
hkid: { type: DataTypes.STRING, allowNull: false },
mobile: { type: DataTypes.STRING, allowNull: false },
age: { type: DataTypes.INTEGER, allowNull: false },
description: { type: DataTypes.STRING, allowNull: false },
//
bruisesScratchesMinorBurns: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
chestPain: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
headache: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
myMuiCheck: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
nauseaAndVomiting: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
runnyOrStuffyNose: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
soreThroat: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
},
{ timestamps: false },
);
(async () => {
try {
await sequelize_config.authenticate();
console.log('Connection has been established successfully.');
await Item.drop();
await sequelize_config.sync();
for (let i = 5; i < 8; i++) {
await Item.create({
name: `semi-urgent-patient ${i}`,
hkid: `A123456(${i})`,
mobile: `9123456${i}`,
age: i + 20,
description: `semi-urgent queue ${i}`,
});
}
await sequelize_config.close();
} catch (error) {
console.error('Unable to connect to the database:', error);
}
})();

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -ex
rm -rf .next node_modules/*
yarn --dev
yarn build
yarn demo

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -ex
yarn --dev
yarn seed
yarn dev

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"baseUrl": ".",
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"paths": {
"@/config/*": ["./config/*"],
"@/public/*": ["./public"]
},
"plugins": []
},
"include": ["**/*.js", "**/*.jsx"],
"exclude": ["node_modules", "**/*copy*", "**/*.del"]
}

View File

@@ -0,0 +1,3 @@
module.exports = {
reactStrictMode: true,
};

View File

@@ -0,0 +1,69 @@
{
"name": "next-mui-boilerplate",
"private": true,
"description": "A JavaScript Nextjs boilerplate complete with material ui, eslint, airbnb react style guides and husky pre-commit hooks",
"keywords": [
"nextjs",
"mui",
"material-ui",
"airbnb-style-guides",
"husky",
"prettier",
"eslint"
],
"scripts": {
"build:w": "npx nodemon -w . --exec 'yarn build'",
"build": "yarn run clear && next build",
"clear": "rm -rf .next",
"demo": "yarn seed && yarn start",
"dev": "next dev -H 0.0.0.0",
"format": "prettier --ignore-path .prettierignore --write .",
"lint-fix:w": "npx nodemon -w . --exec 'yarn lint-fix'",
"lint-fix": "eslint --fix --ext .js,.jsx .",
"lint": "eslint **/*.js --report-unused-disable-directives",
"prepare_disabled": "husky install",
"seed": "cd db_seed && node ./auth.js && node ./patient_queue.js && node ./semi_urgent_case_queue.js && node ./non_urgent_case_queue.js",
"seed1": "cd db_seed && node ./auth.js && node ./patient_queue.js",
"start": "next start"
},
"lint-staged": {
"*js": [
"yarn lint --fix",
"yarn format"
]
},
"dependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/icons-material": "^6.1.3",
"@mui/lab": "^6.0.0-beta.12",
"@mui/material": "^5.1.0",
"@mui/styles": "^5.1.0",
"bcrypt": "^5.1.1",
"dotenv": "^16.4.5",
"formik": "^2.4.6",
"mysql2": "^3.11.3",
"next": "12.0.3",
"prettier-plugin-organize-imports": "^4.1.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"sequelize": "^6.37.4",
"styled-components": "^5.3.3",
"typescript": "^5.6.3",
"yup": "^1.4.0"
},
"devDependencies": {
"eslint": "<8.0.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-next": "12.0.2",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "^7.0.0",
"lint-staged": "^11.2.6",
"prettier": "^2.4.1"
}
}

View File

@@ -0,0 +1,136 @@
import React, { useState } from 'react';
import { ChevronLeftOutlined } from '@mui/icons-material';
import MenuIcon from '@mui/icons-material/Menu';
import {
Box,
Button,
Drawer,
IconButton,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
} from '@mui/material';
import { useRouter } from 'next/dist/client/router';
import Head from 'next/head';
export default function Home() {
const [open, setOpen] = React.useState(false);
const router = useRouter();
const [changing_page, setChangingPage] = useState(false);
const toggleDrawer = newOpen => () => {
setOpen(newOpen);
};
const DrawerList = (
<Box sx={{ width: 250 }} role='presentation' onClick={toggleDrawer(false)}>
<List>
<ListItem key='dashboard' disablePadding>
<ListItemButton
onClick={() => {
setChangingPage(true);
router.push('/AdminLogin');
}}
>
<ListItemIcon>
<ChevronLeftOutlined />
</ListItemIcon>
<ListItemText primary='Logout' />
</ListItemButton>
</ListItem>
</List>
</Box>
);
return (
<>
<Head>
<title>admin dashboard</title>
<meta name='description' content='Generated by create next app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<Box className='main'>
<Box
sx={{
//
display: 'flex',
flexDirection: 'flex-start',
alignItems: 'center',
gap: '1rem',
//
height: '3rem',
}}
>
<IconButton aria-label='menu' onClick={toggleDrawer(true)}>
<MenuIcon />
</IconButton>
<Box sx={{ fontWeight: 'bold', fontSize: '1.2rem' }}>Admin dashboard</Box>
</Box>
<Box
sx={{
height: '90vh',
width: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Box sx={{}}>
<Box style={{ paddingTop: '3rem', width: '90vw' }}>
<Button
variant='contained'
disabled={changing_page}
onClick={() => {
setChangingPage(true);
router.push('/SemiUrgentCaseList');
}}
fullWidth
>
<Box sx={{ fontSize: '1.1rem', padding: '1rem' }}>Semi-urgent case</Box>
</Button>
</Box>
<Box style={{ paddingTop: '3rem', width: '90vw' }}>
<Button
variant='contained'
disabled={changing_page}
onClick={() => {
setChangingPage(true);
router.push('/NonUrgentCaseList');
}}
fullWidth
>
<Box style={{ fontSize: '1.1rem', padding: '1rem' }}>Non-urgent case</Box>
</Button>
</Box>
<Box style={{ paddingTop: '3rem', width: '90vw' }}>
<Button
variant='contained'
disabled={changing_page}
onClick={() => {
setChangingPage(true);
router.push('/SearchCase');
}}
fullWidth
>
<Box style={{ fontSize: '1.1rem', padding: '1rem' }}>Search case</Box>
</Button>
</Box>
</Box>
</Box>
</Box>
<Drawer open={open} onClose={toggleDrawer(false)}>
{DrawerList}
</Drawer>
</>
);
}

View File

@@ -0,0 +1,168 @@
import { Box, Button, TextField } from '@mui/material';
import Head from 'next/head';
import { ChevronLeftOutlined } from '@mui/icons-material';
import LoginIcon from '@mui/icons-material/Login';
import { useFormik } from 'formik';
import { useRouter } from 'next/dist/client/router';
import React from 'react';
import is_development_plant from 'utils/is_development_plant';
import * as yup from 'yup';
let default_init_values = {
username: '',
passwod: '',
};
if (is_development_plant) {
console.log('development plant');
default_init_values = {
username: 'admi',
password: 'nimda',
};
}
const validationSchema = yup.object({
username: yup
.string('Enter your username')
.min(5, 'Username should be of minimum 5 characters length')
.required('Username is required'),
password: yup
.string('Enter your password')
.min(5, 'Password should be of minimum 5 characters length')
.required('Password is required'),
});
export default function Home() {
const router = useRouter();
const [changing_page, setChangingPage] = React.useState(false);
const formik = useFormik({
initialValues: default_init_values,
validationSchema,
onSubmit: values => {
fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
})
.then(res => res.json())
.then(data => {
if (data.success) {
router.push('/AdminHome');
} else {
alert(data.message);
formik.resetForm();
}
})
.catch(err => {
console.error(err);
alert('server error');
});
},
});
return (
<>
<Head>
<title>admin login page</title>
<meta name='description' content='Generated by create next app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<Box
sx={{
height: '90vh',
//
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '1rem',
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
//
width: '100%',
padding: '0 2rem',
}}
>
<form onSubmit={formik.handleSubmit}>
<Box
sx={{
textAlign: 'center',
fontWeight: 'bold',
fontSize: '1.2rem',
marginTop: '2rem',
marginBottom: '2rem',
}}
>
Admin Login Page
</Box>
<TextField
fullWidth
variant='standard'
id='username'
name='username'
value={formik.values.username}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.username && Boolean(formik.errors.username)}
helperText={formik.touched.username && formik.errors.username}
label='Username'
inputProps={{ sx: { textAlign: 'center' } }}
/>
<TextField
fullWidth
variant='standard'
id='password'
name='password'
value={formik.values.password}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.password && Boolean(formik.errors.password)}
helperText={formik.touched.password && formik.errors.password}
type='password'
label='Password'
inputProps={{ sx: { textAlign: 'center' } }}
/>
<Box
style={{
paddingTop: '3rem',
display: 'flex',
justifyContent: 'space-around',
}}
>
<Button
disabled={changing_page || formik.isSubmitting}
variant='outlined'
startIcon={<ChevronLeftOutlined />}
onClick={() => {
setChangingPage(true);
router.push('/');
}}
>
Back
</Button>
<Button
variant='contained'
type='submit'
startIcon={<LoginIcon />}
disabled={!(formik.isValid && formik.dirty && !formik.isSubmitting) || changing_page}
>
Login
</Button>
</Box>
</form>
</Box>
</Box>
</>
);
}

View File

@@ -0,0 +1,246 @@
import React, { useEffect, useState } from 'react';
import { Box, Button, TextField } from '@mui/material';
import Head from 'next/head';
import { ChevronLeftOutlined } from '@mui/icons-material';
import Loading from 'components/Loading';
import { useFormik } from 'formik';
import { useRouter } from 'next/dist/client/router';
import * as yup from 'yup';
const validationSchema = yup.object({
patient_name: yup
.string('Enter your name')
.min(2, 'Name should be of minimum 2 characters length')
.required('Name is required'),
patient_hkid: yup
.string('Enter your HKID')
.matches(/^[A-Z]{1,2}[0-9]{6}\([0-9]\)$/, 'HKID should be in format of XX123456(1-9)')
.required('HKID is required'),
patient_age: yup
.number('Enter your age')
.min(1, 'Age should be greater than 0')
.max(90, 'Age should be less than 90')
.required('Age is required'),
patient_mobile: yup
.string('Enter your mobile number')
.matches(/^[0-9]{8}$/, 'Mobile number should be 8 numbers')
.required('Mobile number is required'),
});
export default () => {
const router = useRouter();
const { id } = router.query;
const [loading, setLoading] = React.useState(true);
const [changing_page, setChangingPage] = useState(false);
const formik = useFormik({
enableReinitialize: true,
initialValues: {
patient_name: '',
patient_hkid: '',
patient_age: 0,
patient_mobile: '',
bruisesScratchesMinorBurns: false,
chestPain: false,
headache: false,
myMuiCheck: false,
nauseaAndVomiting: false,
runnyOrStuffyNose: false,
soreThroat: false,
},
validationSchema,
onSubmit: values => {
fetch('/api/patient_queue/update_queue_item', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, queue_type: 'non-urgent', values }),
})
.then(response => response.json())
.then(data => {
if (data.status) {
alert('saved !');
router.reload();
} else {
// alert(data.message);
alert('sorry there are something wrong, please try again');
}
})
.catch(err => {
console.error(err);
alert('server error');
});
},
});
useEffect(() => {
console.log({ t: `/api/patient_queue/read_queue_item?queue_type=non-urgent&id=${id}` });
if (id) {
fetch(`/api/patient_queue/read_queue_item?queue_type=non-urgent&id=${id}`, {
method: 'GET',
})
.then(response => response.json())
.then(data => {
if (data.status) {
console.log(data);
const {
name,
hkid,
mobile,
age,
bruisesScratchesMinorBurns,
chestPain,
headache,
myMuiCheck,
nauseaAndVomiting,
runnyOrStuffyNose,
soreThroat,
} = data.queueItem;
formik.setValues({
patient_name: name,
patient_hkid: hkid,
patient_mobile: mobile,
patient_age: age,
bruisesScratchesMinorBurns,
chestPain,
headache,
myMuiCheck,
nauseaAndVomiting,
runnyOrStuffyNose,
soreThroat,
});
} else {
alert(data.message);
}
setLoading(false);
})
.catch(err => {
console.error(err);
alert('server error');
})
.finally(() => {
setLoading(false);
});
}
}, [id]);
if (loading) {
return <Loading />;
}
return (
<>
<Head>
<title>dashboard - non-urgent case</title>
<meta name='description' content='Generated by create next app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '90vh',
}}
>
<form onSubmit={formik.handleSubmit}>
<Box sx={{ marginLeft: '2rem', marginRight: '2rem' }}>
<Box
sx={{
fontWeight: 'bold',
fontSize: '1.1rem',
marginTop: '1rem',
marginBottom: '1rem',
}}
>
Edit Registration
</Box>
<TextField
fullWidth
disabled={changing_page || formik.isSubmitting}
variant='standard'
id='patient_name'
name='patient_name'
label='Patient name'
value={formik.values.patient_name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.patient_name && Boolean(formik.errors.patient_name)}
helperText={formik.touched.patient_name && formik.errors.patient_name}
/>
<TextField
fullWidth
disabled={changing_page || formik.isSubmitting}
variant='standard'
id='patient_hkid'
name='patient_hkid'
label='HKID'
value={formik.values.patient_hkid}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.patient_hkid && Boolean(formik.errors.patient_hkid)}
helperText={formik.touched.patient_hkid && formik.errors.patient_hkid}
/>
<TextField
fullWidth
disabled={changing_page || formik.isSubmitting}
variant='standard'
id='patient_age'
name='patient_age'
label='Age'
value={formik.values.patient_age}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.patient_age && Boolean(formik.errors.patient_age)}
helperText={formik.touched.patient_age && formik.errors.patient_age}
/>
<TextField
fullWidth
disabled={changing_page || formik.isSubmitting}
variant='standard'
id='patient_mobile'
name='patient_mobile'
label='Mobile'
value={formik.values.patient_mobile}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.patient_mobile && Boolean(formik.errors.patient_mobile)}
helperText={formik.touched.patient_mobile && formik.errors.patient_mobile}
/>
<Box sx={{ marginTop: '1rem', display: 'flex', justifyContent: 'space-around' }}>
<Button
disabled={changing_page}
onClick={() => {
setChangingPage(true);
router.push('/NonUrgentCaseList');
}}
variant='outline'
startIcon={<ChevronLeftOutlined />}
>
Back
</Button>
{/* */}
<Button
//
color='primary'
variant='contained'
type='submit'
disabled={!(formik.isValid && formik.dirty && !formik.isSubmitting) || changing_page}
>
Save
</Button>
</Box>
</Box>
</form>
</Box>
</>
);
};

View File

@@ -0,0 +1,40 @@
import { ChevronLeftOutlined } from '@mui/icons-material';
import { Box, Button } from '@mui/material';
import { useRouter } from 'next/dist/client/router';
import { useState } from 'react';
export default () => {
const [changing_page, setChangingPage] = useState(false);
const router = useRouter();
return (
<Box
sx={{
width: '100%',
height: '90vh',
//
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '3rem',
//
fontWeight: 'bold',
fontSize: '1.2rem',
}}
>
<Box>Queue is empty</Box>
<Button
onClick={() => {
setChangingPage(true);
router.push('/AdminHome');
}}
startIcon={<ChevronLeftOutlined />}
variant='outlined'
disabled={changing_page}
>
Back
</Button>
</Box>
);
};

View File

@@ -0,0 +1,114 @@
import MenuIcon from '@mui/icons-material/Menu';
import React, { useEffect, useState } from 'react';
import { ChevronLeftOutlined } from '@mui/icons-material';
import { Box, Drawer, IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import Loading from 'components/Loading';
import NonUrgentQueueItemCard from 'components/NonUrgentQueueItemCard';
import { useRouter } from 'next/dist/client/router';
import Head from 'next/head';
import QueueIsEmpty from './QueueIsEmpty';
export default () => {
const [open, setOpen] = React.useState(false);
const [p_queue, setPQueue] = React.useState([]);
const router = useRouter();
const [loading, setLoading] = React.useState(true);
const [changing_page, setChangingPage] = useState(false);
const toggleDrawer = newOpen => () => {
setOpen(newOpen);
};
useEffect(() => {
fetch('/api/patient_queue/non_urgent_case')
.then(response => response.json())
.then(data => {
setPQueue(data.patient_queues);
setLoading(false);
});
}, []);
const DrawerList = (
<Box sx={{ width: 250 }} role='presentation' onClick={toggleDrawer(false)}>
<List>
<ListItem key='dashboard' disablePadding>
<ListItemButton
disabled={changing_page}
onClick={() => {
setChangingPage(true);
router.push('/AdminHome');
}}
>
<ListItemIcon>
<ChevronLeftOutlined />
</ListItemIcon>
<ListItemText primary='back to dashboard' />
</ListItemButton>
</ListItem>
</List>
</Box>
);
if (loading) {
return <Loading />;
}
return (
<>
<Head>
<title>dashboard - non-urgent case</title>
<meta name='description' content='Generated by create next app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<Box className='main'>
<Box
sx={{
//
display: 'flex',
flexDirection: 'flex-start',
alignItems: 'center',
gap: '1rem',
//
height: '3rem',
}}
>
<IconButton aria-label='menu' onClick={toggleDrawer(true)}>
<MenuIcon />
</IconButton>
<Box sx={{ fontWeight: 'bold', fontSize: '1.2rem' }}>All Non-Urgent Cases</Box>
</Box>
{/* */}
<Box style={{ height: '95vh', overflowY: 'scroll' }}>
{p_queue.length === 0 ? (
<QueueIsEmpty />
) : (
<>
{p_queue.map(queue_data => (
<NonUrgentQueueItemCard key={queue_data} queue_data={queue_data} />
))}
<Box
style={{
width: '100%',
display: 'inline-flex',
justifyContent: 'center',
marginTop: '1rem',
marginBottom: '1rem',
}}
>
--end of list--
</Box>
</>
)}
</Box>
helloworld
</Box>
<Drawer open={open} onClose={toggleDrawer(false)}>
{DrawerList}
</Drawer>
</>
);
};

View File

@@ -0,0 +1,40 @@
import { Box, Button } from '@mui/material';
import { useRouter } from 'next/dist/client/router';
import { useState } from 'react';
export default () => {
const router = useRouter();
const [changing_page, setChangingPage] = useState(false);
return (
<>
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'row', justifyContent: 'center', marginTop: '2rem' }}>
<Box sx={{ fontWeight: 'bold', fontSize: '1.5rem' }}>Patient Landing</Box>
</Box>
<Box
sx={{
height: '80vh',
width: '100%',
display: 'inline-flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Box>
<Button
onClick={() => {
setChangingPage(true);
router.push('/PatientRegister');
}}
variant='contained'
disableElevation
disabled={changing_page}
>
Proceed to register
</Button>
</Box>
</Box>
</>
);
};

View File

@@ -0,0 +1,119 @@
import React, { useEffect, useState } from 'react';
import { Box, Button } from '@mui/material';
import Loading from 'components/Loading';
import { useRouter } from 'next/dist/client/router';
import Head from 'next/head';
export default () => {
const [loading, setLoading] = React.useState(true);
const router = useRouter();
const [changing_page, setChangingPage] = useState(false);
const [queue_type, setQueueType] = useState('');
const [queue_id, setQueueId] = useState('');
useEffect(() => {
let temp;
temp = localStorage.getItem('queue_type');
if (temp) setQueueType(temp);
temp = localStorage.getItem('queue_id');
if (temp) setQueueId(temp);
setLoading(false);
}, []);
if (loading) {
return <Loading />;
}
return (
<>
<Head>
<title>dashboard - non-urgent case</title>
<meta name='description' content='Generated by create next app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '90vh',
}}
>
<Box sx={{ fontWeight: 'bold', fontSize: '1.2rem' }}>Queue Number</Box>
<Box
style={{
marginTop: '1.2rem',
fontWeight: 'bold',
fontSize: '6rem',
backgroundColor: 'lightgray',
width: '75vw',
height: '75vw',
borderRadius: '1rem',
//
display: 'inline-flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
//
}}
>
{queue_id}
</Box>
<Box sx={{ marginTop: '1rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
{queue_type !== '' ? (
<>
<Box sx={{ fontWeight: 'bold' }}>Your waiting list category</Box>
<Box sx={{ fontWeight: 'bold', fontSize: '1.2rem' }}>{queue_type}</Box>
</>
) : (
<></>
)}
</Box>
<Box
sx={{
marginTop: '1.2rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
fontWeight: 'bold',
color: 'red',
}}
>
<Box sx={{}}>please screen capture </Box>
<Box sx={{}}>for saving your Queue Number</Box>
<Box sx={{ marginTop: '1rem' }}>
<Button
disabled={changing_page}
onClick={() => {
localStorage.removeItem('queue_id');
localStorage.removeItem('queue_name');
localStorage.removeItem('queue_hkid');
localStorage.removeItem('queue_mobile');
localStorage.removeItem('queue_description');
alert('Please be informed that the queue information cleared');
setChangingPage(true);
router.push('/');
}}
disableElevation
variant='contained'
>
Clear Queue Information
</Button>
</Box>
</Box>
</Box>
</>
);
};

View File

@@ -0,0 +1,318 @@
import React, { useEffect } from 'react';
import MailIcon from '@mui/icons-material/Mail';
import InboxIcon from '@mui/icons-material/MoveToInbox';
import {
Box,
Button,
Checkbox,
Divider,
Drawer,
FormControlLabel,
FormGroup,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
TextField,
} from '@mui/material';
import Head from 'next/head';
import { ChevronLeftOutlined } from '@mui/icons-material';
import Loading from 'components/Loading';
import { useFormik } from 'formik';
import { useRouter } from 'next/dist/client/router';
import * as yup from 'yup';
const validationSchema = yup.object({
patient_name: yup
.string('Enter your name')
.min(2, 'Name should be of minimum 2 characters length')
.required('Name is required'),
patient_hkid: yup
.string('Enter your HKID')
.matches(/^[A-Z]{1,2}[0-9]{6}\([0-9]\)$/, 'HKID should be in format of XX123456(1-9)')
.required('HKID is required'),
patient_age: yup
.number('Enter your age')
.min(1, 'Age should be greater than 0')
.max(90, 'Age should be less than 90')
.required('Age is required'),
patient_mobile: yup
.string('Enter your mobile number')
.matches(/^[0-9]{8}$/, 'Mobile number should be 8 numbers')
.required('Mobile number is required'),
});
export default () => {
const [open, setOpen] = React.useState(false);
const [loading, setLoading] = React.useState(true);
const [one_symptoms_selected, setOneSymptomsSelected] = React.useState(false);
const router = useRouter();
const toggleDrawer = newOpen => () => {
setOpen(newOpen);
};
const formik = useFormik({
initialValues: {
patient_name: '',
patient_hkid: '',
patient_age: 0,
patient_mobile: '',
bruisesScratchesMinorBurns: false,
chestPain: false,
headache: false,
myMuiCheck: false,
nauseaAndVomiting: false,
runnyOrStuffyNose: true,
soreThroat: false,
},
validationSchema,
onSubmit: values => {
// alert(JSON.stringify(values, null, 2));
fetch('/api/patient_queue/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ values }),
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (data.queue) {
alert(`registered successfully, will assign you to ${data.queue} queue`);
} else {
alert(`registered successfully`);
}
localStorage.setItem('queue_type', data.queue);
localStorage.setItem('queue_id', data.result.id);
localStorage.setItem('queue_name', data.result.name);
localStorage.setItem('queue_hkid', data.result.hkid);
localStorage.setItem('queue_mobile', data.result.mobile);
localStorage.setItem('queue_description', data.result.description);
router.push('/PatientQueueDisplay');
} else {
// alert(data.message);
alert('sorry there are something wrong, please try again');
}
})
.catch(err => {
console.error(err);
alert('server error');
});
},
});
useEffect(() => {
setOneSymptomsSelected(
!(
formik.values.bruisesScratchesMinorBurns ||
formik.values.chestPain ||
formik.values.headache ||
formik.values.myMuiCheck ||
formik.values.nauseaAndVomiting ||
formik.values.runnyOrStuffyNose ||
formik.values.soreThroat
),
);
}, [formik.values]);
const DrawerList = (
<Box sx={{ width: 250 }} role='presentation' onClick={toggleDrawer(false)}>
<List>
{['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
<Divider />
<List>
{['All mail', 'Trash', 'Spam'].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
);
useEffect(() => {
if (localStorage.getItem('queue_id')) {
router.push('/PatientQueueDisplay');
} else {
setLoading(false);
}
}, []);
if (loading) {
return <Loading />;
}
return (
<>
<Head>
<title>dashboard - non-urgent case</title>
<meta name='description' content='Generated by create next app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '90vh',
}}
>
<form onSubmit={formik.handleSubmit}>
<Box sx={{ marginLeft: '2rem', marginRight: '2rem' }}>
<Box
sx={{
fontWeight: 'bold',
fontSize: '1.1rem',
marginTop: '1rem',
marginBottom: '1rem',
}}
>
Electronic Diagnosis Registration
</Box>
<TextField
fullWidth
variant='standard'
id='patient_name'
name='patient_name'
label='Patient name'
value={formik.values.patient_name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.patient_name && Boolean(formik.errors.patient_name)}
helperText={formik.touched.patient_name && formik.errors.patient_name}
/>
<TextField
fullWidth
variant='standard'
id='patient_hkid'
name='patient_hkid'
label='HKID'
value={formik.values.patient_hkid}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.patient_hkid && Boolean(formik.errors.patient_hkid)}
helperText={formik.touched.patient_hkid && formik.errors.patient_hkid}
/>
<TextField
fullWidth
variant='standard'
id='patient_age'
name='patient_age'
label='Age'
value={formik.values.patient_age}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.patient_age && Boolean(formik.errors.patient_age)}
helperText={formik.touched.patient_age && formik.errors.patient_age}
/>
<TextField
fullWidth
variant='standard'
id='patient_mobile'
name='patient_mobile'
label='Mobile'
value={formik.values.patient_mobile}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.patient_mobile && Boolean(formik.errors.patient_mobile)}
helperText={formik.touched.patient_mobile && formik.errors.patient_mobile}
/>
<Box sx={{ marginTop: '2rem' }}>
<FormGroup>
<Box sx={{ marginTop: '1rem', marginBottom: '1rem' }}>Reason for seeking medical advice</Box>
{formik.dirty && one_symptoms_selected ? (
<Box sx={{ color: 'red', fontSize: '0.8rem', fontWeight: 'bold' }}>
Please select at least one symptom
</Box>
) : (
<></>
)}
<FormControlLabel
control={<Checkbox />}
label='Runny or stuffy nose'
checked={formik.values.runnyOrStuffyNose}
onChange={() => formik.setFieldValue('runnyOrStuffyNose', !formik.values.runnyOrStuffyNose)}
/>
<FormControlLabel
control={<Checkbox />}
label='sore throat'
checked={formik.values.soreThroat}
onChange={() => formik.setFieldValue('soreThroat', !formik.values.soreThroat)}
/>
<FormControlLabel
control={<Checkbox />}
label='nausea and vomiting'
checked={formik.values.nauseaAndVomiting}
onChange={() => formik.setFieldValue('nauseaAndVomiting', !formik.values.nauseaAndVomiting)}
/>
<FormControlLabel
control={<Checkbox />}
label='Headache (Semi-urgent)'
checked={formik.values.headache}
onChange={() => formik.setFieldValue('headache', !formik.values.headache)}
/>
<FormControlLabel
control={<Checkbox />}
label='chest pain (Semi-urgent)'
checked={formik.values.chestPain}
onChange={() => formik.setFieldValue('chestPain', !formik.values.chestPain)}
/>
<FormControlLabel
control={<Checkbox />}
label='Bruises, scratches, minor burns (Semi-urgent)'
checked={formik.values.bruisesScratchesMinorBurns}
onChange={() =>
formik.setFieldValue('bruisesScratchesMinorBurns', !formik.values.bruisesScratchesMinorBurns)
}
/>
</FormGroup>
</Box>
<Box sx={{ marginTop: '1rem', display: 'flex', justifyContent: 'space-around' }}>
<Button
disabled={formik.isSubmitting}
onClick={() => router.push('/')}
variant='outline'
startIcon={<ChevronLeftOutlined />}
>
Cancel
</Button>
{/* */}
<Button
color='primary'
variant='contained'
type='submit'
disabled={!(formik.isValid && formik.dirty && !formik.isSubmitting)}
>
Submit
</Button>
</Box>
</Box>
</form>
</Box>
<Drawer open={open} onClose={toggleDrawer(false)}>
{DrawerList}
</Drawer>
</>
);
};

View File

@@ -0,0 +1,21 @@
import { Box } from '@mui/material';
export default () => (
<Box
sx={{
width: '100%',
height: '30vh',
//
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '3rem',
//
fontWeight: 'bold',
fontSize: '1.2rem',
}}
>
<Box>No result found</Box>
</Box>
);

View File

@@ -0,0 +1,21 @@
import { Box } from '@mui/material';
export default () => (
<Box
sx={{
width: '100%',
height: '30vh',
//
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '3rem',
//
fontWeight: 'bold',
fontSize: '1.2rem',
}}
>
<Box>Press Search to start</Box>
</Box>
);

View File

@@ -0,0 +1,256 @@
import MenuIcon from '@mui/icons-material/Menu';
import React, { useState } from 'react';
import { ChevronLeftOutlined } from '@mui/icons-material';
import {
Box,
Button,
Checkbox,
Drawer,
FormControlLabel,
IconButton,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
TextField,
} from '@mui/material';
import NonUrgentQueueItemCard from 'components/NonUrgentQueueItemCard';
import SemiUrgentQueueItemCard from 'components/SemiUrgentQueueItemCard';
import { useFormik } from 'formik';
import { useRouter } from 'next/dist/client/router';
import Head from 'next/head';
import * as yup from 'yup';
import NoResultFound from './NoResultFound';
import PressSearchToStart from './PressSearchToStart';
const default_init_values = {
semi_urgent_case: true,
non_urgent_case: true,
hkid: '',
mobile: '',
};
const validationSchema = yup.object({});
const search_input_row_sx = {
width: '100%',
padding: '1rem',
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-start',
gap: '0.25rem',
fontWeight: 'bold',
};
const row_button_sx = {
width: '100%',
padding: '1rem',
display: 'flex',
justifyContent: 'space-around',
gap: '0.5rem',
};
export default () => {
const [open, setOpen] = React.useState(false);
const [p_queue, setPQueue] = React.useState([]);
const router = useRouter();
const [not_search_yet, setNotSearchYet] = React.useState(true);
const [changing_page, setChangingPage] = useState(false);
const toggleDrawer = newOpen => () => {
setOpen(newOpen);
};
const formik = useFormik({
initialValues: default_init_values,
validationSchema,
onSubmit: values => {
setNotSearchYet(false);
fetch('/api/patient_queue/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
})
.then(res => res.json())
.then(data => {
setPQueue(data.patient_queues);
formik.setSubmitting(false);
})
.catch(err => {
console.error(err);
alert('server error');
});
},
});
const DrawerList = (
<Box sx={{ width: 250 }} role='presentation' onClick={toggleDrawer(false)}>
<List>
<ListItem key='dashboard' disablePadding>
<ListItemButton
disabled={changing_page}
onClick={() => {
setChangingPage(true);
router.push('/AdminHome');
}}
>
<ListItemIcon>
<ChevronLeftOutlined />
</ListItemIcon>
<ListItemText primary='back to dashboard' />
</ListItemButton>
</ListItem>
</List>
</Box>
);
return (
<>
<Head>
<title>dashboard - non-urgent case</title>
<meta name='description' content='Generated by create next app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<Box className='main'>
<Box
sx={{
//
display: 'flex',
flexDirection: 'flex-start',
alignItems: 'center',
gap: '1rem',
//
height: '3rem',
}}
>
<IconButton aria-label='menu' onClick={toggleDrawer(true)}>
<MenuIcon />
</IconButton>
<Box sx={{ fontWeight: 'bold', fontSize: '1.2rem' }}>Search case</Box>
</Box>
<Box>
<form onSubmit={formik.handleSubmit}>
<Box sx={search_input_row_sx}>
<Box>Search by case type:</Box>
<FormControlLabel
control={<Checkbox sx={{ padding: '0.25rem 1rem' }} size='small' />}
label='semi-urgent case'
checked={formik.values.semi_urgent_case}
onChange={() => formik.setFieldValue('semi_urgent_case', !formik.values.semi_urgent_case)}
/>
<FormControlLabel
control={<Checkbox sx={{ padding: '0.25rem 1rem' }} size='small' />}
label='non-urgent case'
checked={formik.values.non_urgent_case}
onChange={() => formik.setFieldValue('non_urgent_case', !formik.values.non_urgent_case)}
/>
</Box>
<Box sx={search_input_row_sx}>
<TextField
fullWidth
variant='standard'
id='hkid'
name='hkid'
label='Search by HKID: Please input HKID'
size='small'
value={formik.values.hkid}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.hkid && Boolean(formik.errors.hkid)}
helperText={formik.touched.hkid && formik.errors.hkid}
/>
</Box>
<Box sx={search_input_row_sx}>
<TextField
fullWidth
variant='standard'
id='mobile'
name='mobile'
label='Search by Mobile: Please input no.'
size='small'
value={formik.values.mobile}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.mobile && Boolean(formik.errors.mobile)}
helperText={formik.touched.mobile && formik.errors.mobile}
/>
</Box>
<Box sx={row_button_sx}>
<Button
disabled={!formik.dirty || formik.isSubmitting}
fullWidth
variant='outlined'
onClick={() => {
setNotSearchYet(true);
setPQueue([]);
formik.resetForm();
}}
>
Reset
</Button>
<Button
// disabled={!(formik.isValid && formik.dirty && !formik.isSubmitting) || changing_page}
fullWidth
variant='contained'
color='primary'
type='submit'
disabled={!(formik.isValid && formik.dirty && !formik.isSubmitting)}
>
Search
</Button>
</Box>
</form>
</Box>
<Box style={{ overflowY: 'scroll' }}>
{not_search_yet ? (
<PressSearchToStart />
) : (
<>
{p_queue.length === 0 ? (
<NoResultFound />
) : (
<>
{p_queue.map(queue_data => {
if (queue_data.queue_type === 'non-urgent')
return (
<NonUrgentQueueItemCard key={queue_data} queue_data={queue_data} show_edit_delete={false} />
);
if (queue_data.queue_type === 'semi-urgent')
return (
<SemiUrgentQueueItemCard key={queue_data} queue_data={queue_data} show_edit_delete={false} />
);
return <></>;
})}
<Box
style={{
width: '100%',
display: 'inline-flex',
justifyContent: 'center',
marginTop: '1rem',
marginBottom: '1rem',
}}
>
--end of list--
</Box>
</>
)}
</>
)}
</Box>
</Box>
<Drawer open={open} onClose={toggleDrawer(false)}>
{DrawerList}
</Drawer>
</>
);
};

View File

@@ -0,0 +1,246 @@
import React, { useEffect } from 'react';
import { Box, Button, TextField } from '@mui/material';
import Head from 'next/head';
import { ChevronLeftOutlined } from '@mui/icons-material';
import Loading from 'components/Loading';
import { useFormik } from 'formik';
import { useRouter } from 'next/dist/client/router';
import * as yup from 'yup';
const validationSchema = yup.object({
patient_name: yup
.string('Enter your name')
.min(2, 'Name should be of minimum 2 characters length')
.required('Name is required'),
patient_hkid: yup
.string('Enter your HKID')
.matches(/^[A-Z]{1,2}[0-9]{6}\([0-9]\)$/, 'HKID should be in format of XX123456(1-9)')
.required('HKID is required'),
patient_age: yup
.number('Enter your age')
.min(1, 'Age should be greater than 0')
.max(90, 'Age should be less than 90')
.required('Age is required'),
patient_mobile: yup
.string('Enter your mobile number')
.matches(/^[0-9]{8}$/, 'Mobile number should be 8 numbers')
.required('Mobile number is required'),
});
export default () => {
const router = useRouter();
const { id } = router.query;
const [changing_page, setChangingPage] = React.useState(false);
const [loading, setLoading] = React.useState(true);
const formik = useFormik({
enableReinitialize: true,
initialValues: {
patient_name: '',
patient_hkid: '',
patient_age: 0,
patient_mobile: '',
//
bruisesScratchesMinorBurns: false,
chestPain: false,
headache: false,
myMuiCheck: false,
nauseaAndVomiting: false,
runnyOrStuffyNose: false,
soreThroat: false,
},
validationSchema,
onSubmit: values => {
fetch('/api/patient_queue/update_queue_item', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, queue_type: 'semi-urgent', values }),
})
.then(response => response.json())
.then(data => {
if (data.status) {
alert('saved !');
router.reload();
} else {
// alert(data.message);
alert('sorry there are something wrong, please try again');
}
})
.catch(err => {
console.error(err);
alert('server error');
});
},
});
useEffect(() => {
console.log({ t: `/api/patient_queue/read_queue_item?queue_type=semi-urgent&id=${id}` });
if (id) {
fetch(`/api/patient_queue/read_queue_item?queue_type=semi-urgent&id=${id}`, {
method: 'GET',
})
.then(response => response.json())
.then(data => {
if (data.status) {
console.log(data);
const {
name,
hkid,
mobile,
age,
bruisesScratchesMinorBurns,
chestPain,
headache,
myMuiCheck,
nauseaAndVomiting,
runnyOrStuffyNose,
soreThroat,
} = data.queueItem;
formik.setValues({
patient_name: name,
patient_hkid: hkid,
patient_mobile: mobile,
patient_age: age,
bruisesScratchesMinorBurns,
chestPain,
headache,
myMuiCheck,
nauseaAndVomiting,
runnyOrStuffyNose,
soreThroat,
});
} else {
alert(data.message);
}
setLoading(false);
})
.catch(err => {
console.error(err);
alert('server error');
})
.finally(() => {
setLoading(false);
});
}
}, [id]);
if (loading) {
return <Loading />;
}
return (
<>
<Head>
<title>dashboard - non-urgent case</title>
<meta name='description' content='Generated by create next app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '90vh',
}}
>
<form onSubmit={formik.handleSubmit}>
<Box sx={{ marginLeft: '2rem', marginRight: '2rem' }}>
<Box
sx={{
fontWeight: 'bold',
fontSize: '1.1rem',
marginTop: '1rem',
marginBottom: '1rem',
}}
>
Edit Registration
</Box>
<TextField
fullWidth
disabled={changing_page || formik.isSubmitting}
variant='standard'
id='patient_name'
name='patient_name'
label='Patient name'
value={formik.values.patient_name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.patient_name && Boolean(formik.errors.patient_name)}
helperText={formik.touched.patient_name && formik.errors.patient_name}
/>
<TextField
fullWidth
disabled={changing_page || formik.isSubmitting}
variant='standard'
id='patient_hkid'
name='patient_hkid'
label='HKID'
value={formik.values.patient_hkid}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.patient_hkid && Boolean(formik.errors.patient_hkid)}
helperText={formik.touched.patient_hkid && formik.errors.patient_hkid}
/>
<TextField
fullWidth
disabled={changing_page || formik.isSubmitting}
variant='standard'
id='patient_age'
name='patient_age'
label='Age'
value={formik.values.patient_age}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.patient_age && Boolean(formik.errors.patient_age)}
helperText={formik.touched.patient_age && formik.errors.patient_age}
/>
<TextField
fullWidth
disabled={changing_page || formik.isSubmitting}
variant='standard'
id='patient_mobile'
name='patient_mobile'
label='Mobile'
value={formik.values.patient_mobile}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.patient_mobile && Boolean(formik.errors.patient_mobile)}
helperText={formik.touched.patient_mobile && formik.errors.patient_mobile}
/>
<Box sx={{ marginTop: '1rem', display: 'flex', justifyContent: 'space-around' }}>
<Button
disabled={changing_page || formik.isSubmitting}
onClick={() => {
setChangingPage(true);
router.push('/SemiUrgentCaseList');
}}
variant='outline'
startIcon={<ChevronLeftOutlined />}
>
Back
</Button>
{/* */}
<Button
color='primary'
variant='contained'
type='submit'
disabled={!(formik.isValid && formik.dirty && !formik.isSubmitting) || changing_page}
>
Save
</Button>
</Box>
</Box>
</form>
</Box>
</>
);
};

View File

@@ -0,0 +1,40 @@
import { ChevronLeftOutlined } from '@mui/icons-material';
import { Box, Button } from '@mui/material';
import { useRouter } from 'next/dist/client/router';
import { useState } from 'react';
export default () => {
const [changing_page, setChangingPage] = useState(false);
const router = useRouter();
return (
<Box
sx={{
width: '100%',
height: '90vh',
//
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '3rem',
//
fontWeight: 'bold',
fontSize: '1.2rem',
}}
>
<Box>Queue is empty</Box>
<Button
onClick={() => {
setChangingPage(true);
router.push('/AdminHome');
}}
startIcon={<ChevronLeftOutlined />}
variant='outlined'
disabled={changing_page}
>
Back
</Button>
</Box>
);
};

View File

@@ -0,0 +1,112 @@
import React, { useEffect, useState } from 'react';
import { ChevronLeftOutlined } from '@mui/icons-material';
import MenuIcon from '@mui/icons-material/Menu';
import { Box, Drawer, IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import Loading from 'components/Loading';
import SemiUrgentQueueItemCard from 'components/SemiUrgentQueueItemCard';
import { useRouter } from 'next/dist/client/router';
import Head from 'next/head';
import QueueIsEmpty from './QueueIsEmpty';
export default () => {
const [open, setOpen] = React.useState(false);
const [p_queue, setPQueue] = React.useState([]);
const router = useRouter();
const [loading, setLoading] = React.useState(true);
const [changing_page, setChangingPage] = useState(false);
const toggleDrawer = newOpen => () => {
setOpen(newOpen);
};
useEffect(() => {
fetch('/api/patient_queue/semi_urgent_case')
.then(response => response.json())
.then(data => {
setPQueue(data.patient_queues);
setLoading(false);
});
}, []);
const DrawerList = (
<Box sx={{ width: 250 }} role='presentation' onClick={toggleDrawer(false)}>
<List>
<ListItem key='dashboard' disablePadding>
<ListItemButton
disabled={changing_page}
onClick={() => {
setChangingPage(true);
router.push('/AdminHome');
}}
>
<ListItemIcon>
<ChevronLeftOutlined />
</ListItemIcon>
<ListItemText primary='back to dashboard' />
</ListItemButton>
</ListItem>
</List>
</Box>
);
if (loading) {
return <Loading />;
}
return (
<>
<Head>
<title>dashboard - semi-urgent case</title>
<meta name='description' content='Generated by create next app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<Box className='main'>
<Box
sx={{
//
display: 'flex',
flexDirection: 'flex-start',
alignItems: 'center',
gap: '1rem',
//
height: '3rem',
}}
>
<IconButton aria-label='menu' onClick={toggleDrawer(true)}>
<MenuIcon />
</IconButton>
<Box sx={{ fontWeight: 'bold', fontSize: '1.2rem' }}>All Semi-Urgent Cases</Box>
</Box>
<Box style={{ height: '95vh', overflowY: 'scroll' }}>
{p_queue.length === 0 ? (
<QueueIsEmpty />
) : (
<>
{p_queue.map(queue_data => (
<SemiUrgentQueueItemCard key={queue_data} queue_data={queue_data} />
))}
<Box
style={{
width: '100%',
display: 'inline-flex',
justifyContent: 'center',
marginTop: '1rem',
marginBottom: '1rem',
}}
>
--end of list--
</Box>
</>
)}
</Box>
</Box>
<Drawer open={open} onClose={toggleDrawer(false)}>
{DrawerList}
</Drawer>
</>
);
};

View File

@@ -0,0 +1,18 @@
import ThemeProvider from '@/config/StyledMaterialThemeProvider';
import theme from '@/config/theme';
import Head from 'next/head';
function MyApp({ Component, pageProps }) {
return (
<>
<Head>
<meta name='viewport' content='minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no' />
</Head>
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
</>
);
}
export default MyApp;

View File

@@ -0,0 +1,98 @@
import { ServerStyleSheets } from '@mui/styles';
import Document, { Head, Html, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
import theme from '@/config/theme';
export default class MyDocument extends Document {
componentDidMount() {}
render() {
return (
<Html lang='en'>
<Head>
<meta charSet='utf-8' />
{/* PWA primary color */}
<meta name='theme-color' content={theme.palette.primary.main} />
</Head>
<style jsx global>
{`
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fica Sans,
Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
`}
</style>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
MyDocument.getInitialProps = async ctx => {
// Resolution order
//
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render
//
// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render
//
// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render
// Render app and page and get the context of the page with collected side effects.
const sheets = new ServerStyleSheets();
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => sheet.collectStyles(sheets.collect(<App {...props} />)),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: (
<>
{initialProps.styles}
{sheets.getStyleElement()}
{sheet.getStyleElement()}
{/* {flush() || null} */}
</>
),
};
} finally {
sheet.seal();
}
};

View File

@@ -0,0 +1,21 @@
async function helloworld(req) {
try {
return { status: 'OK' };
} catch (error) {
console.error(error);
return { status: 'ERROR' };
}
}
async function handler(req, res) {
try {
let result = await helloworld(req);
return res.status(200).send(result);
} catch (err) {
console.log(err);
return res.status(200).send({ status: 'error', message: 'helloworld error' });
}
}
export default handler;

View File

@@ -0,0 +1,33 @@
import Auth from './model';
async function login(req) {
try {
console.log({ test: req.body });
const { username: incoming_username, password: incoming_password } = req.body;
const users = await Auth.findAll();
for (let i = 0; i < users.length; i += 1) {
const user = users[i];
console.log(users);
if (user.username === incoming_username && user.password === incoming_password) {
return { success: 'login success' };
}
}
return { message: 'login failed' };
} catch (error) {
console.error(error);
return { message: 'login failed' };
}
}
async function handler(req, res) {
try {
const result = await login(req);
return res.status(200).send(result);
} catch (err) {
console.log(err);
return res.status(200).send({ status: 'error', message: 'helloworld error' });
}
}
export default handler;

View File

@@ -0,0 +1,17 @@
import sequelize_config from 'utils/sequelize_config';
const { DataTypes } = require('sequelize');
const Auth = sequelize_config.define(
'Auths',
{
uid: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, unique: true },
username: { type: DataTypes.STRING, allowNull: false },
password: { type: DataTypes.STRING, allowNull: false },
session: { type: DataTypes.STRING, allowNull: false, defaultValue: '' },
role: { type: DataTypes.STRING, allowNull: false },
},
{ timestamps: false },
);
export default Auth;

View File

@@ -0,0 +1,20 @@
async function helloworld(req) {
try {
return { status: 'OK' };
} catch (error) {
console.error(error);
}
}
async function handler(req, res) {
try {
let result = await helloworld(req);
return res.status(200).send(result);
} catch (err) {
console.log(err);
res.status(200).send({ status: 'error', message: 'helloworld error' });
}
}
export default handler;

View File

@@ -0,0 +1,39 @@
import { NonUrgentQueue, SemiUrgentQueue } from './model';
async function deleteQueueById(req, res) {
try {
const { id } = req.query;
const { queue_type } = req.query;
if (!id || !queue_type) {
return res.status(400).send({ status: 'error', message: 'id and queue_type are required' });
}
switch (queue_type) {
case 'semi-urgent':
await SemiUrgentQueue.destroy({ where: { id } });
return { status: 'OK' };
case 'non-urgent':
await NonUrgentQueue.destroy({ where: { id } });
return { status: 'OK' };
default:
return res.status(400).send({ status: 'error', message: 'invalid queue_type' });
}
} catch (error) {
console.error(error);
return { status: 'error' };
}
}
async function handler(req, res) {
try {
const result = await deleteQueueById(req, res);
return res.status(200).send(result);
} catch (err) {
console.log(err);
return res.status(200).send({ status: 'error', message: 'list error' });
}
}
export default handler;

View File

@@ -0,0 +1,25 @@
import { PatientQueue } from './model';
async function list() {
try {
const patient_queues = await PatientQueue.findAll();
return { status: 'OK', patient_queues };
} catch (error) {
console.error(error);
return { status: 'ERROR' };
}
}
async function handler(req, res) {
try {
const result = await list(req);
return res.status(200).send(result);
} catch (err) {
console.log(err);
return res.status(200).send({ status: 'error', message: 'list error' });
}
}
export default handler;

View File

@@ -0,0 +1,55 @@
const { DataTypes } = require('sequelize');
const sequelize_config = require('../../../utils/sequelize_config');
const PatientQueue = sequelize_config.define(
'PatientQueue',
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, unique: true },
description: { type: DataTypes.STRING, allowNull: false },
},
{ timestamps: false },
);
const SemiUrgentQueue = sequelize_config.define(
'SemiUrgentQueue',
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, unique: true },
name: { type: DataTypes.STRING, allowNull: false },
hkid: { type: DataTypes.STRING, allowNull: false },
mobile: { type: DataTypes.STRING, allowNull: false },
age: { type: DataTypes.INTEGER, allowNull: false },
description: { type: DataTypes.STRING, allowNull: false },
//
bruisesScratchesMinorBurns: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
chestPain: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
headache: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
myMuiCheck: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
nauseaAndVomiting: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
runnyOrStuffyNose: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
soreThroat: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
},
{ timestamps: false },
);
const NonUrgentQueue = sequelize_config.define(
'NonUrgentQueue',
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, unique: true },
name: { type: DataTypes.STRING, allowNull: false },
hkid: { type: DataTypes.STRING, allowNull: false },
mobile: { type: DataTypes.STRING, allowNull: false },
age: { type: DataTypes.INTEGER, allowNull: false },
description: { type: DataTypes.STRING, allowNull: false },
//
bruisesScratchesMinorBurns: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
chestPain: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
headache: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
myMuiCheck: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
nauseaAndVomiting: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
runnyOrStuffyNose: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
soreThroat: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
},
{ timestamps: false },
);
export { NonUrgentQueue, PatientQueue, SemiUrgentQueue };

View File

@@ -0,0 +1,24 @@
import { NonUrgentQueue } from './model';
async function list() {
try {
const patient_queues = await NonUrgentQueue.findAll();
return { status: 'OK', patient_queues };
} catch (error) {
console.error(error);
}
}
async function handler(req, res) {
try {
const result = await list(req);
return res.status(200).send(result);
} catch (err) {
console.log(err);
return res.status(200).send({ status: 'error', message: 'list error' });
}
}
export default handler;

View File

@@ -0,0 +1,46 @@
import { NonUrgentQueue, SemiUrgentQueue } from './model';
async function readQueueById(req, res) {
try {
const { id } = req.query;
const { queue_type } = req.query;
if (!id || !queue_type) {
return res.status(400).send({ status: 'error', message: 'id and queue_type are required' });
}
let queueItem;
switch (queue_type) {
case 'semi-urgent':
queueItem = await SemiUrgentQueue.findOne({ where: { id } });
break;
case 'non-urgent':
queueItem = await NonUrgentQueue.findOne({ where: { id } });
break;
default:
return res.status(400).send({ status: 'error', message: 'invalid queue_type' });
}
if (!queueItem) {
return res.status(404).send({ status: 'error', message: 'No queue item found' });
}
return { status: 'OK', queueItem };
} catch (error) {
console.error(error);
return { status: 'error' };
}
}
async function handler(req, res) {
try {
const result = await readQueueById(req);
return res.status(200).send(result);
} catch (err) {
console.log(err);
return res.status(200).send({ status: 'error', message: 'list error' });
}
}
export default handler;

View File

@@ -0,0 +1,77 @@
import { NonUrgentQueue, SemiUrgentQueue } from './model';
async function list(req) {
let output = {};
const {
patient_name: name,
patient_hkid: hkid,
patient_age: age,
patient_mobile: mobile,
//
bruisesScratchesMinorBurns,
chestPain,
headache,
myMuiCheck,
nauseaAndVomiting,
runnyOrStuffyNose,
soreThroat,
} = req.body.values;
try {
if (headache || chestPain || bruisesScratchesMinorBurns) {
console.log('headache || chestPain || bruisesScratchesMinorBurns, classified to a SemiUrgentQueue');
const result = await SemiUrgentQueue.create({
name,
hkid,
mobile,
age,
description: '',
bruisesScratchesMinorBurns,
chestPain,
headache,
myMuiCheck,
nauseaAndVomiting,
runnyOrStuffyNose,
soreThroat,
});
output = { success: 'OK', queue: 'semi-urgent', result };
} else {
console.log('headache == false, classified to a NonUrgentQueue');
const result = await NonUrgentQueue.create({
name,
hkid,
mobile,
age,
description: '',
bruisesScratchesMinorBurns,
chestPain,
headache,
myMuiCheck,
nauseaAndVomiting,
runnyOrStuffyNose,
soreThroat,
});
output = { success: 'OK', queue: 'non-urgent', result };
}
} catch (error) {
output = { message: 'error' };
}
return output;
}
async function handler(req, res) {
try {
const result = await list(req);
return res.status(200).send(result);
} catch (err) {
console.log(err);
return res.status(200).send({ status: 'error', message: 'list error' });
}
}
export default handler;

View File

@@ -0,0 +1,51 @@
import { Op, Sequelize } from 'sequelize';
import { NonUrgentQueue, SemiUrgentQueue } from './model';
async function search_queue(req) {
try {
let n_u_result = [];
let s_u_result = [];
const { semi_urgent_case, non_urgent_case } = req.body;
const criteria = ['hkid', 'mobile']
.filter(key => req.body[key] !== '')
.map(key => ({
[key]: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col(key)), 'LIKE', `%${req.body[key].toLowerCase()}%`),
}));
if (non_urgent_case) {
n_u_result = await NonUrgentQueue.findAll({ where: { [Op.or]: criteria } });
// if (n_u_result === undefined) n_u_result = [];
n_u_result.forEach(q => {
q.dataValues.queue_type = 'non-urgent';
});
}
if (semi_urgent_case) {
s_u_result = await SemiUrgentQueue.findAll({ where: { [Op.or]: criteria } });
// if (s_u_result === undefined) s_u_result = [];
s_u_result.forEach(q => {
q.dataValues.queue_type = 'semi-urgent';
});
}
console.log({ s_u_result });
return { status: 'OK', patient_queues: [...n_u_result, ...s_u_result] };
} catch (error) {
console.error(error);
return { status: 'ERROR' };
}
}
async function handler(req, res) {
try {
const result = await search_queue(req);
return res.status(200).send(result);
} catch (err) {
console.log(err);
return res.status(200).send({ status: 'error', message: 'list error' });
}
}
export default handler;

View File

@@ -0,0 +1,25 @@
import { SemiUrgentQueue } from './model';
async function list() {
try {
const patient_queues = await SemiUrgentQueue.findAll();
return { status: 'OK', patient_queues };
} catch (error) {
console.error(error);
return { status: 'ERROR' };
}
}
async function handler(req, res) {
try {
const result = await list(req);
return res.status(200).send(result);
} catch (err) {
console.log(err);
return res.status(200).send({ status: 'error', message: 'list error' });
}
}
export default handler;

View File

@@ -0,0 +1,15 @@
fetch("http://localhost/api/patient_queue/read_queue_item?queue_type=non-urgent&id=4", {
"headers": {
"sec-ch-ua": "\"Google Chrome\";v=\"129\", \"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"129\"",
"sec-ch-ua-mobile": "?1",
"sec-ch-ua-platform": "\"Android\"",
"Referer": "http://localhost/NonUrgentCaseEdit/1",
"Referrer-Policy": "strict-origin-when-cross-origin"
},
"body": null,
"method": "GET"
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

View File

@@ -0,0 +1,23 @@
fetch("http://localhost/api/patient_queue/register", {
"headers": {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9,zh-TW;q=0.8,zh-CN;q=0.7,zh;q=0.6",
"cache-control": "no-cache",
"content-type": "application/json",
"pragma": "no-cache",
"sec-ch-ua": "\"Google Chrome\";v=\"129\", \"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"129\"",
"sec-ch-ua-mobile": "?1",
"sec-ch-ua-platform": "\"Android\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"cookie": "pma_lang=en; phpMyAdmin=51ff7da1fa656cafa38dce7a6be8a79d",
"Referer": "http://localhost/PatientRegister",
"Referrer-Policy": "strict-origin-when-cross-origin"
},
"body": "{\"values\":{\"patient_name\":\"p1\",\"patient_hkid\":\"A123456(7)\",\"patient_age\":\"32\",\"patient_mobile\":\"91234567\",\"bruisesScratchesMinorBurns\":true,\"chestPain\":false,\"headache\":false,\"myMuiCheck\":false,\"nauseaAndVomiting\":false,\"runnyOrStuffyNose\":true,\"soreThroat\":false}}",
"method": "POST"
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

View File

@@ -0,0 +1,23 @@
fetch('http://localhost/api/patient_queue/search', {
headers: {
accept: '*/*',
'accept-language': 'en-US,en;q=0.9,zh-TW;q=0.8,zh-CN;q=0.7,zh;q=0.6',
'cache-control': 'no-cache',
'content-type': 'application/json',
pragma: 'no-cache',
'sec-ch-ua': '"Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"',
'sec-ch-ua-mobile': '?1',
'sec-ch-ua-platform': '"Android"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
cookie: 'pma_lang=en',
Referer: 'http://localhost/SearchCase',
'Referrer-Policy': 'strict-origin-when-cross-origin',
},
body: '{"semi_urgent_case":true,"non_urgent_case":false,"hkid":"","mobile":"91234567"}',
method: 'POST',
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

View File

@@ -0,0 +1,21 @@
fetch("http://localhost/api/patient_queue/update_queue_item", {
"headers": {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9,zh-TW;q=0.8,zh-CN;q=0.7,zh;q=0.6",
"content-type": "application/json",
"sec-ch-ua": "\"Google Chrome\";v=\"129\", \"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"129\"",
"sec-ch-ua-mobile": "?1",
"sec-ch-ua-platform": "\"Android\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"cookie": "pma_lang=en; phpMyAdmin=51ff7da1fa656cafa38dce7a6be8a79d",
"Referer": "http://localhost/NonUrgentCaseEdit/4",
"Referrer-Policy": "strict-origin-when-cross-origin"
},
"body": "{\"id\":\"4\",\"queue_type\":\"non-urgent\",\"values\":{\"patient_name\":\"non-urgent-patient 3 update\",\"patient_hkid\":\"A123456(3)\",\"patient_mobile\":\"91234563\",\"patient_age\":13}}",
"method": "POST"
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

View File

@@ -0,0 +1,89 @@
import { NonUrgentQueue, SemiUrgentQueue } from './model';
async function updateQueueById(req, res) {
try {
const { id, queue_type } = req.body;
console.log({ t: req.body });
if (!id || !queue_type) {
return res.status(400).send({ status: 'error', message: 'id and queue_type are required' });
}
const {
patient_name: name,
patient_hkid: hkid,
patient_age: age,
patient_mobile: mobile,
bruisesScratchesMinorBurns,
chestPain,
headache,
myMuiCheck,
nauseaAndVomiting,
runnyOrStuffyNose,
soreThroat,
} = req.body.values;
switch (queue_type) {
case 'non-urgent':
await NonUrgentQueue.update(
{
name,
hkid,
age,
mobile,
bruisesScratchesMinorBurns,
chestPain,
headache,
myMuiCheck,
nauseaAndVomiting,
runnyOrStuffyNose,
soreThroat,
},
{ where: { id } },
);
return { status: 'OK' };
case 'semi-urgent':
await SemiUrgentQueue.update(
{
name,
hkid,
age,
mobile,
bruisesScratchesMinorBurns,
chestPain,
headache,
myMuiCheck,
nauseaAndVomiting,
runnyOrStuffyNose,
soreThroat,
},
{ where: { id } },
);
return { status: 'OK' };
default:
return res.status(400).send({ status: 'error', message: 'invalid queue_type' });
}
} catch (error) {
console.error(error);
return { status: 'error' };
}
}
async function handler(req, res) {
if (req.method === 'POST') {
try {
const result = await updateQueueById(req, res);
return res.status(200).send(result);
} catch (err) {
console.log(err);
return res.status(200).send({ status: 'error', message: 'list error' });
}
} else {
return res.status(405).send({ status: 'error', message: 'method not allowed' });
}
}
export default handler;

View File

@@ -0,0 +1,104 @@
import React from 'react';
import MailIcon from '@mui/icons-material/Mail';
import InboxIcon from '@mui/icons-material/MoveToInbox';
import { Box, Divider, Drawer, Link, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import Head from 'next/head';
import { useFormik } from 'formik';
import * as yup from 'yup';
const validationSchema = yup.object({
// email: yup.string('Enter your email').email('Enter a valid email').required('Email is required'),
// password: yup
// .string('Enter your password')
// .min(8, 'Password should be of minimum 8 characters length')
// .required('Password is required'),
});
export default function Home() {
const [open, setOpen] = React.useState(false);
const toggleDrawer = newOpen => () => {
setOpen(newOpen);
};
const formik = useFormik({
initialValues: {
email: 'foobar@example.com',
password: 'foobar',
patient_name: 'default patient name',
patient_hkid: 'A213456(7)',
patient_age: 37,
patient_mobile: '91234567',
bruisesScratchesMinorBurns: false,
chestPain: false,
headache: false,
myMuiCheck: false,
nauseaAndVomiting: false,
runnyOrStuffyNose: false,
soreThroat: false,
},
validationSchema: validationSchema,
onSubmit: values => {
alert(JSON.stringify(values, null, 2));
},
});
const DrawerList = (
<Box sx={{ width: 250 }} role='presentation' onClick={toggleDrawer(false)}>
<List>
{['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
<Divider />
<List>
{['All mail', 'Trash', 'Spam'].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
);
return (
<>
<Head>
<title>dashboard - non-urgent case</title>
<meta name='description' content='Generated by create next app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '90vh',
}}
>
<Link href='/PatientRegister'>PatientRegister</Link>
<Link href='/PatientQueueDisplay'>PatientQueueDisplay</Link>
<Link href='/AdminLogin'>AdminLogin</Link>
<Link href='/AdminHome'>AdminHome</Link>
<Link href='/SemiUrgentCaseList'>SemiUrgentCaseList</Link>
<Link href='/NonUrgentCaseList'>NonUrgentCaseList</Link>
</Box>
<Drawer open={open} onClose={toggleDrawer(false)}>
{DrawerList}
</Drawer>
</>
);
}

View File

@@ -0,0 +1,5 @@
export default () => `
padding:0;
margin:0;
`;

View File

@@ -0,0 +1,55 @@
import { Box, Button } from '@mui/material';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useState } from 'react';
export default () => {
const [changing_page, setChangingPage] = useState(false);
const router = useRouter();
return (
<>
<Head>
<title>dashboard - non-urgent case</title>
<meta name='description' content='Generated by create next app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '90vh',
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Box sx={{ fontWeight: 'bold', fontSize: '1.2rem' }}>Demo Patient Queue system</Box>
<Button
onClick={() => {
setChangingPage(true);
router.push('/PatientLanding');
}}
variant='contained'
disableElevation
disabled={changing_page}
>
I am patient
</Button>
<Button
onClick={() => {
setChangingPage(true);
router.push('/AdminLogin');
}}
variant='contained'
disableElevation
disabled={changing_page}
>
I am admin
</Button>
</Box>
</Box>
</>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
export default process.env.NODE_ENV === 'development';

View File

@@ -0,0 +1,38 @@
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize('app_db', 'db_user', 'db_user_pass', {
host: 'mysql',
port: 3306,
dialect: 'mysql',
});
const User = sequelize.define(
'User',
{
firstName: { type: Sequelize.STRING, allowNull: false },
lastName: { type: Sequelize.STRING, allowNull: false },
email: { type: Sequelize.STRING, allowNull: false },
role: { type: Sequelize.STRING, allowNull: false },
},
{ timestamps: false },
);
(async () => {
try {
await sequelize.authenticate();
console.log('Connection has been established successfully.');
// create table
await sequelize.sync();
const user = await User.create({ firstName: 'John', lastName: 'Doe' });
console.log('Created user: ', user);
const users = await User.findAll();
console.log('Found all users: ', users);
await sequelize.close();
} catch (error) {
console.error('Unable to connect to the database:', error);
}
})();

View File

@@ -0,0 +1,9 @@
const { Sequelize } = require('sequelize');
const sequelize_config = new Sequelize('app_db', 'db_user', 'db_user_pass', {
host: 'mysql',
port: 3306,
dialect: 'mysql',
});
module.exports = sequelize_config;

File diff suppressed because it is too large Load Diff

13
task1/project/003_src/dc_up.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -ex
docker compose pull
docker compose build
docker compose kill
docker compose down --volumes
sudo rm -rf ./volumes/mysql
docker compose up -d

View File

@@ -0,0 +1,50 @@
# TODO: rename me
name: jamespong14205
volumes:
client_node_modules:
services:
client:
image: node:20-buster-slim
restart: always
volumes:
- ./client:/usr/bin/app
- client_node_modules:/usr/bin/app/node_modules
command: sleep infinity
working_dir: /usr/bin/app
ports:
- 80:3000
mysql:
image: mysql:latest
# container_name: db
restart: always
environment:
MYSQL_ROOT_PASSWORD: my_secret_password
MYSQL_DATABASE: app_db
MYSQL_USER: db_user
MYSQL_PASSWORD: db_user_pass
MYSQL_ROOT_HOST: "%"
ports:
- "6033:3306"
volumes:
- ./volumes/mysql:/var/lib/mysql
phpmyadmin:
image: phpmyadmin/phpmyadmin
# container_name: pma
restart: always
links:
- mysql
environment:
PMA_PORT: 3306
PMA_ARBITRARY: 1
#
PMA_HOST: mysql
PMA_USER: db_user
PMA_PASSWORD: db_user_pass
ports:
- 8080:80

6
task1/project/003_src/setup.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -ex
npx create-next-app@latest helloworld-js \
--no-eslint --use-yarn --js --no-tailwind

11
task1/quotation.md Normal file
View File

@@ -0,0 +1,11 @@
(1 page + 1 logic)
(1 page + 2 logic)
(2 pages + 2 logic)
(1 page + 2 logic)
(1 page + 2 logic)
6 page + 10 logic
HKD 2000
stack: ionic + node + windows