diff --git a/webclient/src/Admin.js b/webclient/src/Admin.js
index 0fc8bc3..e620be1 100644
--- a/webclient/src/Admin.js
+++ b/webclient/src/Admin.js
@@ -3,577 +3,53 @@ import { BrowserRouter as Router, Switch, Route, Link, useParams, useHistory } f
import './light.css';
import { Button, Container, Checkbox, Dimmer, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react';
import moment from 'moment';
-import { statusColor, BasicTable, staticUrl, requester } from './utils.js';
-import { TransactionList, TransactionEditor } from './Transactions.js';
+import { apiUrl, statusColor, BasicTable, staticUrl, requester } from './utils.js';
import { NotFound } from './Misc.js';
-export function AdminReportedTransactions(props) {
+export function Admin(props) {
const { token, user } = props;
- const [transactions, setTransactions] = useState(false);
+ const [backup, setBackup] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
- requester('/transactions/', 'GET', token)
+ requester('/backup/', 'GET', token)
.then(res => {
- setTransactions(res.results);
- setError(false);
+ setBackup(res.url);
})
.catch(err => {
console.log(err);
- setError(true);
});
}, []);
return (
-
+
- {!error ?
- transactions ?
-
-
-
- :
- Loading...
+
+ Spaceport backups are created daily. 14 days are kept on the server.
+
+ {backup ?
+
+
+ Download link:
+
+ {backup}
+
+
+
+
+ Automate with wget (keep secret, that's your login token):
+
+ wget \
+
--header="Authorization: Token {token}" \
+
{apiUrl}/backup/
+
+
+
:
-
+ Loading...
}
);
};
-
-export function AdminTransactions(props) {
- const { token, result, refreshResult } = props;
- const transactions = result.transactions;
- const [open, setOpen] = useState(false);
- const [input, setInput] = useState({ date: moment().format('YYYY-MM-DD'), info_source: 'Web' });
- const [error, setError] = useState(false);
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
- const { id } = useParams();
-
- const handleSubmit = (e) => {
- if (loading) return;
- setLoading(true);
- setSuccess(false);
- const data = { ...input, member_id: id };
- requester('/transactions/', 'POST', token, data)
- .then(res => {
- setSuccess(res.id);
- setInput({});
- setLoading(false);
- setError(false);
- refreshResult();
- })
- .catch(err => {
- setLoading(false);
- console.log(err);
- setError(err.data);
- });
- };
-
- return (
-
-
-
-
- Submit
-
- {success &&
Added! View the transaction.
}
-
-
-
-
- {transactions.length ?
- open ?
-
- :
-
- :
-
None
- }
-
-
- );
-};
-
-function AdminCardDetail(props) {
- const { token, result, card } = props;
- const [input, setInput] = useState({ ...card });
- const [error, setError] = useState(false);
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
- const id = card.id;
-
- const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value });
- const handleUpload = (e, v) => setInput({ ...input, [v.name]: e.target.files[0] });
- const handleChange = (e) => handleValues(e, e.currentTarget);
- const handleCheck = (e, v) => setInput({ ...input, [v.name]: v.checked });
-
- const handleSubmit = (e) => {
- if (loading) return;
- setLoading(true);
- setSuccess(false);
- const data = { ...input, member_id: result.member.id };
- requester('/cards/'+id+'/', 'PUT', token, data)
- .then(res => {
- setLoading(false);
- setSuccess(true);
- setError(false);
- setInput(res);
- })
- .catch(err => {
- setLoading(false);
- console.log(err);
- setError(err.data);
- });
- };
-
- const handleDelete = (e) => {
- e.preventDefault();
-
- requester('/cards/'+id+'/', 'DELETE', token)
- .then(res => {
- setInput(false);
- })
- .catch(err => {
- console.log(err);
- });
- };
-
- const makeProps = (name) => ({
- name: name,
- onChange: handleChange,
- value: input[name] || '',
- error: error[name],
- });
-
- const statusOptions = [
- { key: '0', text: 'Card Active', value: 'card_active' },
- { key: '1', text: 'Card Blocked', value: 'card_blocked' },
- { key: '2', text: 'Card Inactive', value: 'card_inactive' },
- { key: '3', text: 'Card Member Blocked', value: 'card_member_blocked' },
- ];
-
- return (
- input ?
-
-
-
-
-
-
-
- {success ? 'Saved.' : 'Save'}
-
-
-
- Delete
-
-
-
-
- Notes: {input.notes || 'None'}
-
-
- :
-
- Deleted card: {card.card_number}
-
- );
-};
-
-export function AdminMemberCards(props) {
- const { token, result, refreshResult } = props;
- const cards = result.cards;
- const startDimmed = Boolean(result.member.paused_date && cards.length);
- const [dimmed, setDimmed] = useState(startDimmed);
- const [input, setInput] = useState({ active_status: 'card_active' });
- const [error, setError] = useState(false);
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
- const { id } = useParams();
-
- useEffect(() => {
- const startDimmed = Boolean(result.member.paused_date && cards.length);
- setDimmed(startDimmed);
- }, [result.member]);
-
- const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value });
- const handleUpload = (e, v) => setInput({ ...input, [v.name]: e.target.files[0] });
- const handleChange = (e) => handleValues(e, e.currentTarget);
- const handleCheck = (e, v) => setInput({ ...input, [v.name]: v.checked });
-
- const handleSubmit = (e) => {
- if (loading) return;
- setLoading(true);
- setSuccess(false);
- const data = { ...input, member_id: result.member.id };
- requester('/cards/', 'POST', token, data)
- .then(res => {
- setLoading(false);
- setSuccess(true);
- setError(false);
- refreshResult();
- })
- .catch(err => {
- setLoading(false);
- console.log(err);
- setError(err.data);
- });
- };
-
- const makeProps = (name) => ({
- name: name,
- onChange: handleChange,
- value: input[name] || '',
- error: error[name],
- });
-
- const statusOptions = [
- { key: '0', text: 'Card Active', value: 'card_active' },
- { key: '1', text: 'Card Blocked', value: 'card_blocked' },
- { key: '2', text: 'Card Inactive', value: 'card_inactive' },
- { key: '3', text: 'Card Member Blocked', value: 'card_member_blocked' },
- ];
-
- return (
-
-
-
-
-
-
-
-
-
- Submit
-
- {success &&
Success!
}
-
-
-
-
-
- {cards.length ?
- cards.map(x =>
-
- )
- :
- None
- }
-
-
-
- Member paused, {cards.length} card{cards.length === 1 ? '' : 's'} ignored anyway.
-
-
-
-
-
-
-
-
- );
-};
-
-export function AdminMemberPause(props) {
- const { token, result, refreshResult } = props;
- const [error, setError] = useState(false);
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
- const [yousure, setYousure] = useState(false);
- const { id } = useParams();
-
- useEffect(() => {
- setLoading(false);
- }, [result.member]);
-
- const handlePause = (e) => {
- if (yousure) {
- if (loading) return;
- setLoading(true);
- setSuccess(false);
- requester('/members/' + id + '/pause/', 'POST', token, {})
- .then(res => {
- setYousure(false);
- setSuccess(true);
- setError(false);
- refreshResult();
- })
- .catch(err => {
- setLoading(false);
- console.log(err);
- setError(true);
- });
- } else {
- setYousure(true);
- }
- };
-
- const handleUnpause = (e) => {
- if (loading) return;
- setLoading(true);
- setSuccess(false);
- requester('/members/' + id + '/unpause/', 'POST', token, {})
- .then(res => {
- setSuccess(true);
- setError(false);
- refreshResult();
- })
- .catch(err => {
- setLoading(false);
- console.log(err);
- setError(true);
- });
- };
-
- return (
-
-
Pause / Unpause Membership
-
-
Pause members who are inactive, former, or on vacation.
-
-
- {result.member.paused_date ?
-
- :
-
- }
-
-
- {success &&
Success!
}
- {error &&
Error, something went wrong.
}
-
- );
-};
-
-export function AdminMemberForm(props) {
- const { token, result, refreshResult } = props;
- const [input, setInput] = useState(result.member);
- const [error, setError] = useState(false);
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
- const { id } = useParams();
-
- useEffect(() => {
- setInput(result.member);
- }, [result.member]);
-
- const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value });
- const handleUpload = (e, v) => setInput({ ...input, [v.name]: e.target.files[0] });
- const handleChange = (e) => handleValues(e, e.currentTarget);
- const handleCheck = (e, v) => setInput({ ...input, [v.name]: v.checked });
-
- const handleSubmit = (e) => {
- if (loading) return;
- setLoading(true);
- setSuccess(false);
- const data = { ...input, email: input.email.toLowerCase() };
- requester('/members/' + id + '/', 'PATCH', token, data)
- .then(res => {
- setLoading(false);
- setSuccess(true);
- setError(false);
- refreshResult();
- })
- .catch(err => {
- setLoading(false);
- console.log(err);
- setError(err.data);
- });
- };
-
- const makeProps = (name) => ({
- name: name,
- onChange: handleChange,
- value: input[name] || '',
- error: error[name],
- });
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Submit
-
- {success &&
Success!
}
-
-
- );
-};
-
-export function AdminMemberInfo(props) {
- const member = props.result.member;
-
- return (
-
-
-
-
-
-
- Name:
- {member.first_name} {member.last_name}
-
-
- Status:
-
-
- {member.status || 'Unknown'}
-
-
-
-
- Expire Date:
- {member.expire_date}
-
- {member.paused_date &&
- Paused Date:
- {member.paused_date}
- }
-
-
- Phone:
- {member.phone}
-
-
-
- Address:
- {member.street_address}
-
-
- City:
- {member.city}
-
-
- Postal:
- {member.postal_code}
-
-
-
- Minor:
- {member.is_minor ? 'Yes' : 'No'}
-
- {member.is_minor &&
- Birthdate:
- {member.birthdate}
- }
- {member.is_minor &&
- Guardian:
- {member.guardian_name}
- }
-
-
- Emergency Contact Name:
- {member.emergency_contact_name || 'None'}
-
-
- Emergency Contact Phone:
- {member.emergency_contact_phone || 'None'}
-
-
-
-
- {member.member_forms &&
-
- View application forms
-
-
}
-
- );
-};
diff --git a/webclient/src/AdminMembers.js b/webclient/src/AdminMembers.js
new file mode 100644
index 0000000..e68ebf9
--- /dev/null
+++ b/webclient/src/AdminMembers.js
@@ -0,0 +1,480 @@
+import React, { useState, useEffect } from 'react';
+import { BrowserRouter as Router, Switch, Route, Link, useParams, useHistory } from 'react-router-dom';
+import './light.css';
+import { Button, Container, Checkbox, Dimmer, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react';
+import moment from 'moment';
+import { statusColor, BasicTable, staticUrl, requester } from './utils.js';
+import { NotFound } from './Misc.js';
+
+function AdminCardDetail(props) {
+ const { token, result, card } = props;
+ const [input, setInput] = useState({ ...card });
+ const [error, setError] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [success, setSuccess] = useState(false);
+ const id = card.id;
+
+ const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value });
+ const handleUpload = (e, v) => setInput({ ...input, [v.name]: e.target.files[0] });
+ const handleChange = (e) => handleValues(e, e.currentTarget);
+ const handleCheck = (e, v) => setInput({ ...input, [v.name]: v.checked });
+
+ const handleSubmit = (e) => {
+ if (loading) return;
+ setLoading(true);
+ setSuccess(false);
+ const data = { ...input, member_id: result.member.id };
+ requester('/cards/'+id+'/', 'PUT', token, data)
+ .then(res => {
+ setLoading(false);
+ setSuccess(true);
+ setError(false);
+ setInput(res);
+ })
+ .catch(err => {
+ setLoading(false);
+ console.log(err);
+ setError(err.data);
+ });
+ };
+
+ const handleDelete = (e) => {
+ e.preventDefault();
+
+ requester('/cards/'+id+'/', 'DELETE', token)
+ .then(res => {
+ setInput(false);
+ })
+ .catch(err => {
+ console.log(err);
+ });
+ };
+
+ const makeProps = (name) => ({
+ name: name,
+ onChange: handleChange,
+ value: input[name] || '',
+ error: error[name],
+ });
+
+ const statusOptions = [
+ { key: '0', text: 'Card Active', value: 'card_active' },
+ { key: '1', text: 'Card Blocked', value: 'card_blocked' },
+ { key: '2', text: 'Card Inactive', value: 'card_inactive' },
+ { key: '3', text: 'Card Member Blocked', value: 'card_member_blocked' },
+ ];
+
+ return (
+ input ?
+
+
+
+
+
+
+
+ {success ? 'Saved.' : 'Save'}
+
+
+
+ Delete
+
+
+
+
+ Notes: {input.notes || 'None'}
+
+
+ :
+
+ Deleted card: {card.card_number}
+
+ );
+};
+
+export function AdminMemberCards(props) {
+ const { token, result, refreshResult } = props;
+ const cards = result.cards;
+ const startDimmed = Boolean(result.member.paused_date && cards.length);
+ const [dimmed, setDimmed] = useState(startDimmed);
+ const [input, setInput] = useState({ active_status: 'card_active' });
+ const [error, setError] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [success, setSuccess] = useState(false);
+ const { id } = useParams();
+
+ useEffect(() => {
+ const startDimmed = Boolean(result.member.paused_date && cards.length);
+ setDimmed(startDimmed);
+ }, [result.member]);
+
+ const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value });
+ const handleUpload = (e, v) => setInput({ ...input, [v.name]: e.target.files[0] });
+ const handleChange = (e) => handleValues(e, e.currentTarget);
+ const handleCheck = (e, v) => setInput({ ...input, [v.name]: v.checked });
+
+ const handleSubmit = (e) => {
+ if (loading) return;
+ setLoading(true);
+ setSuccess(false);
+ const data = { ...input, member_id: result.member.id };
+ requester('/cards/', 'POST', token, data)
+ .then(res => {
+ setLoading(false);
+ setSuccess(true);
+ setError(false);
+ refreshResult();
+ })
+ .catch(err => {
+ setLoading(false);
+ console.log(err);
+ setError(err.data);
+ });
+ };
+
+ const makeProps = (name) => ({
+ name: name,
+ onChange: handleChange,
+ value: input[name] || '',
+ error: error[name],
+ });
+
+ const statusOptions = [
+ { key: '0', text: 'Card Active', value: 'card_active' },
+ { key: '1', text: 'Card Blocked', value: 'card_blocked' },
+ { key: '2', text: 'Card Inactive', value: 'card_inactive' },
+ { key: '3', text: 'Card Member Blocked', value: 'card_member_blocked' },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+ Submit
+
+ {success &&
Success!
}
+
+
+
+
+
+ {cards.length ?
+ cards.map(x =>
+
+ )
+ :
+ None
+ }
+
+
+
+ Member paused, {cards.length} card{cards.length === 1 ? '' : 's'} ignored anyway.
+
+
+
+
+
+
+
+
+ );
+};
+
+export function AdminMemberPause(props) {
+ const { token, result, refreshResult } = props;
+ const [error, setError] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [success, setSuccess] = useState(false);
+ const [yousure, setYousure] = useState(false);
+ const { id } = useParams();
+
+ useEffect(() => {
+ setLoading(false);
+ }, [result.member]);
+
+ const handlePause = (e) => {
+ if (yousure) {
+ if (loading) return;
+ setLoading(true);
+ setSuccess(false);
+ requester('/members/' + id + '/pause/', 'POST', token, {})
+ .then(res => {
+ setYousure(false);
+ setSuccess(true);
+ setError(false);
+ refreshResult();
+ })
+ .catch(err => {
+ setLoading(false);
+ console.log(err);
+ setError(true);
+ });
+ } else {
+ setYousure(true);
+ }
+ };
+
+ const handleUnpause = (e) => {
+ if (loading) return;
+ setLoading(true);
+ setSuccess(false);
+ requester('/members/' + id + '/unpause/', 'POST', token, {})
+ .then(res => {
+ setSuccess(true);
+ setError(false);
+ refreshResult();
+ })
+ .catch(err => {
+ setLoading(false);
+ console.log(err);
+ setError(true);
+ });
+ };
+
+ return (
+
+
Pause / Unpause Membership
+
+
Pause members who are inactive, former, or on vacation.
+
+
+ {result.member.paused_date ?
+
+ :
+
+ }
+
+
+ {success &&
Success!
}
+ {error &&
Error, something went wrong.
}
+
+ );
+};
+
+export function AdminMemberForm(props) {
+ const { token, result, refreshResult } = props;
+ const [input, setInput] = useState(result.member);
+ const [error, setError] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [success, setSuccess] = useState(false);
+ const { id } = useParams();
+
+ useEffect(() => {
+ setInput(result.member);
+ }, [result.member]);
+
+ const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value });
+ const handleUpload = (e, v) => setInput({ ...input, [v.name]: e.target.files[0] });
+ const handleChange = (e) => handleValues(e, e.currentTarget);
+ const handleCheck = (e, v) => setInput({ ...input, [v.name]: v.checked });
+
+ const handleSubmit = (e) => {
+ if (loading) return;
+ setLoading(true);
+ setSuccess(false);
+ const data = { ...input, email: input.email.toLowerCase() };
+ requester('/members/' + id + '/', 'PATCH', token, data)
+ .then(res => {
+ setLoading(false);
+ setSuccess(true);
+ setError(false);
+ refreshResult();
+ })
+ .catch(err => {
+ setLoading(false);
+ console.log(err);
+ setError(err.data);
+ });
+ };
+
+ const makeProps = (name) => ({
+ name: name,
+ onChange: handleChange,
+ value: input[name] || '',
+ error: error[name],
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Submit
+
+ {success &&
Success!
}
+
+
+ );
+};
+
+export function AdminMemberInfo(props) {
+ const member = props.result.member;
+
+ return (
+
+
+
+
+
+
+ Name:
+ {member.first_name} {member.last_name}
+
+
+ Status:
+
+
+ {member.status || 'Unknown'}
+
+
+
+
+ Expire Date:
+ {member.expire_date}
+
+ {member.paused_date &&
+ Paused Date:
+ {member.paused_date}
+ }
+
+
+ Phone:
+ {member.phone}
+
+
+
+ Address:
+ {member.street_address}
+
+
+ City:
+ {member.city}
+
+
+ Postal:
+ {member.postal_code}
+
+
+
+ Minor:
+ {member.is_minor ? 'Yes' : 'No'}
+
+ {member.is_minor &&
+ Birthdate:
+ {member.birthdate}
+ }
+ {member.is_minor &&
+ Guardian:
+ {member.guardian_name}
+ }
+
+
+ Emergency Contact Name:
+ {member.emergency_contact_name || 'None'}
+
+
+ Emergency Contact Phone:
+ {member.emergency_contact_phone || 'None'}
+
+
+
+
+ {member.member_forms &&
+
+ View application forms
+
+
}
+
+ );
+};
diff --git a/webclient/src/AdminTransactions.js b/webclient/src/AdminTransactions.js
new file mode 100644
index 0000000..432d0b8
--- /dev/null
+++ b/webclient/src/AdminTransactions.js
@@ -0,0 +1,106 @@
+import React, { useState, useEffect } from 'react';
+import { BrowserRouter as Router, Switch, Route, Link, useParams, useHistory } from 'react-router-dom';
+import './light.css';
+import { Button, Container, Checkbox, Dimmer, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react';
+import moment from 'moment';
+import { statusColor, BasicTable, staticUrl, requester } from './utils.js';
+import { TransactionList, TransactionEditor } from './Transactions.js';
+import { NotFound } from './Misc.js';
+
+export function AdminReportedTransactions(props) {
+ const { token, user } = props;
+ const [transactions, setTransactions] = useState(false);
+ const [error, setError] = useState(false);
+
+ useEffect(() => {
+ requester('/transactions/', 'GET', token)
+ .then(res => {
+ setTransactions(res.results);
+ setError(false);
+ })
+ .catch(err => {
+ console.log(err);
+ setError(true);
+ });
+ }, []);
+
+ return (
+
+
+
+ {!error ?
+ transactions ?
+
+
+
+ :
+ Loading...
+ :
+
+ }
+
+
+ );
+};
+
+export function AdminTransactions(props) {
+ const { token, result, refreshResult } = props;
+ const transactions = result.transactions;
+ const [open, setOpen] = useState(false);
+ const [input, setInput] = useState({ date: moment().format('YYYY-MM-DD'), info_source: 'Web' });
+ const [error, setError] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [success, setSuccess] = useState(false);
+ const { id } = useParams();
+
+ const handleSubmit = (e) => {
+ if (loading) return;
+ setLoading(true);
+ setSuccess(false);
+ const data = { ...input, member_id: id };
+ requester('/transactions/', 'POST', token, data)
+ .then(res => {
+ setSuccess(res.id);
+ setInput({});
+ setLoading(false);
+ setError(false);
+ refreshResult();
+ })
+ .catch(err => {
+ setLoading(false);
+ console.log(err);
+ setError(err.data);
+ });
+ };
+
+ return (
+
+
+
+
+ Submit
+
+ {success &&
Added! View the transaction.
}
+
+
+
+
+ {transactions.length ?
+ open ?
+
+ :
+
+ :
+
None
+ }
+
+
+ );
+};
diff --git a/webclient/src/App.js b/webclient/src/App.js
index 4f9cbb0..b6a380d 100644
--- a/webclient/src/App.js
+++ b/webclient/src/App.js
@@ -9,7 +9,8 @@ import { Account } from './Account.js';
import { Transactions, TransactionDetail } from './Transactions.js';
import { Cards } from './Cards.js';
import { Training } from './Training.js';
-import { AdminReportedTransactions } from './Admin.js';
+import { AdminReportedTransactions } from './AdminTransactions.js';
+import { Admin } from './Admin.js';
import { Courses, CourseDetail } from './Courses.js';
import { Classes, ClassDetail } from './Classes.js';
import { Members, MemberDetail } from './Members.js';
@@ -132,6 +133,12 @@ function App() {
to='/classes'
/>
+ {user && isAdmin(user) && }
+
{user && isAdmin(user) &&
+ {user && isAdmin(user) &&
+
+
+
+ }
+
{user && isAdmin(user) &&
diff --git a/webclient/src/Members.js b/webclient/src/Members.js
index 4216c94..b057992 100644
--- a/webclient/src/Members.js
+++ b/webclient/src/Members.js
@@ -4,7 +4,8 @@ import './light.css';
import { Button, Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Input, Item, Menu, Message, Segment, Table } from 'semantic-ui-react';
import { statusColor, isAdmin, BasicTable, staticUrl, requester } from './utils.js';
import { NotFound, PleaseLogin } from './Misc.js';
-import { AdminMemberInfo, AdminMemberPause, AdminMemberForm, AdminMemberCards, AdminTransactions } from './Admin.js';
+import { AdminMemberInfo, AdminMemberPause, AdminMemberForm, AdminMemberCards } from './AdminMembers.js';
+import { AdminTransactions } from './AdminTransactions.js';
export function MembersDropdown(props) {
const { token, name, onChange, value, initial } = props;