Merge branch 'master' into storage_space
This commit is contained in:
BIN
webclient/public/toast.png
Normal file
BIN
webclient/public/toast.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
@@ -69,31 +69,27 @@ export function AdminVetting(props) {
|
||||
const displayAll = (vetting && vetting.length <= 5) || showAll;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='adminvetting'>
|
||||
{!error ?
|
||||
vetting ?
|
||||
<>
|
||||
<Table collapsing basic='very'>
|
||||
<Table compact collapsing unstackable basic='very'>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Name</Table.HeaderCell>
|
||||
<Table.HeaderCell></Table.HeaderCell>
|
||||
<Table.HeaderCell>Status</Table.HeaderCell>
|
||||
<Table.HeaderCell>Start Date</Table.HeaderCell>
|
||||
<Table.HeaderCell>Status / NMO</Table.HeaderCell>
|
||||
<Table.HeaderCell></Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{(displayAll ? vetting : vetting.slice(0,5)).map(x =>
|
||||
{(displayAll ? vetting : vetting.slice(0,5)).sort((a, b) => a.last_name > b.last_name ? 1 : -1).map(x =>
|
||||
<Table.Row key={x.id}>
|
||||
<Table.Cell><Link to={'/members/'+x.id}>{x.preferred_name} {x.last_name}</Link></Table.Cell>
|
||||
<Table.Cell><a href={'mailto:'+x.email}>Email</a></Table.Cell>
|
||||
<Table.Cell>
|
||||
<Icon name='circle' color={statusColor[x.status]} />
|
||||
{x.status || 'Unknown'}
|
||||
{x.orientation_date ? '✅' : '❌'}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{x.current_start_date}</Table.Cell>
|
||||
<Table.Cell><AdminVet {...props} member={x} refreshVetting={refreshVetting} /></Table.Cell>
|
||||
</Table.Row>
|
||||
)}
|
||||
@@ -344,6 +340,7 @@ export function Admin(props) {
|
||||
|
||||
<Header size='medium'>Ready to Vet</Header>
|
||||
<p>Members who are Current or Due, and past their probationary period.</p>
|
||||
<p>Sorted by last name.</p>
|
||||
<AdminVetting {...props} />
|
||||
|
||||
|
||||
|
@@ -124,7 +124,7 @@ let prevAutoscan = '';
|
||||
export function AdminMemberCards(props) {
|
||||
const { token, result, refreshResult } = props;
|
||||
const cards = result.cards;
|
||||
const startDimmed = Boolean((result.member.paused_date || !result.member.is_allowed_entry) && cards.length);
|
||||
const startDimmed = Boolean((result.member.paused_date || !result.member.is_allowed_entry || !result.member.vetted_date) && cards.length);
|
||||
const [dimmed, setDimmed] = useState(startDimmed);
|
||||
const [input, setInput] = useState({ active_status: 'card_active' });
|
||||
const [error, setError] = useState(false);
|
||||
@@ -134,7 +134,7 @@ export function AdminMemberCards(props) {
|
||||
const { id } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
const startDimmed = Boolean((result.member.paused_date || !result.member.is_allowed_entry) && cards.length);
|
||||
const startDimmed = Boolean((result.member.paused_date || !result.member.is_allowed_entry || !result.member.vetted_date) && cards.length);
|
||||
setDimmed(startDimmed);
|
||||
}, [result.member]);
|
||||
|
||||
@@ -298,7 +298,7 @@ export function AdminMemberCards(props) {
|
||||
|
||||
<Dimmer active={dimmed}>
|
||||
<p>
|
||||
Member paused or not allowed entry, {cards.length} card{cards.length === 1 ? '' : 's'} ignored anyway.
|
||||
Member paused, unvetted or not allowed entry. {cards.length} card{cards.length === 1 ? '' : 's'} ignored anyway.
|
||||
</p>
|
||||
<p>
|
||||
<Button size='tiny' onClick={() => setDimmed(false)}>Close</Button>
|
||||
@@ -363,18 +363,77 @@ export function AdminMemberPause(props) {
|
||||
<div>
|
||||
<Header size='medium'>Pause / Unpause Membership</Header>
|
||||
|
||||
<p>Pause members who are inactive, former, or on vacation.</p>
|
||||
|
||||
<p>
|
||||
{result.member.paused_date ?
|
||||
<Button onClick={handleUnpause} loading={loading}>
|
||||
Unpause
|
||||
</Button>
|
||||
result.member.vetted_date && moment().diff(moment(result.member.paused_date), 'days') > 370 ?
|
||||
<>
|
||||
<p>
|
||||
{result.member.preferred_name} has been away for more than a year and will need to be re-vetted according to our
|
||||
<a href='https://wiki.protospace.ca/Approved_policies/Membership' target='_blank' rel='noopener noreferrer'> policy</a>.
|
||||
</p>
|
||||
<p>
|
||||
<Form.Checkbox
|
||||
name='told1'
|
||||
value={told1}
|
||||
label='Told member to get re-vetted'
|
||||
required
|
||||
onChange={(e, v) => setTold1(v.checked)}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<Form.Checkbox
|
||||
name='told2'
|
||||
value={told2}
|
||||
label='Collected payment for member dues'
|
||||
required
|
||||
onChange={(e, v) => setTold2(v.checked)}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<Button onClick={handleUnpause} loading={loading} disabled={!told1 || !told2}>
|
||||
Unpause
|
||||
</Button>
|
||||
</>
|
||||
:
|
||||
result.member.status == 'Expired Member' ?
|
||||
<>
|
||||
<p>
|
||||
{result.member.preferred_name} has expired due to lapse of payment.
|
||||
</p>
|
||||
<p>
|
||||
<Form.Checkbox
|
||||
name='told1'
|
||||
value={told1}
|
||||
label='Member has paid any back-dues owed'
|
||||
required
|
||||
onChange={(e, v) => setTold1(v.checked)}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<Form.Checkbox
|
||||
name='told2'
|
||||
value={told2}
|
||||
label='Recorded payment transaction on portal'
|
||||
required
|
||||
onChange={(e, v) => setTold2(v.checked)}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<Button onClick={handleUnpause} loading={loading} disabled={!told1 || !told2}>
|
||||
Unpause
|
||||
</Button>
|
||||
</>
|
||||
:
|
||||
<Button onClick={handleUnpause} loading={loading}>
|
||||
Unpause
|
||||
</Button>
|
||||
:
|
||||
<>
|
||||
<p>Pause members who are inactive, former, or on vacation.</p>
|
||||
|
||||
<p>
|
||||
<Form.Checkbox
|
||||
name='told_subscriptions'
|
||||
name='told1'
|
||||
value={told1}
|
||||
label='Told member to stop any PayPal subscriptions'
|
||||
required
|
||||
@@ -383,7 +442,7 @@ export function AdminMemberPause(props) {
|
||||
</p>
|
||||
<p>
|
||||
<Form.Checkbox
|
||||
name='told_shelves'
|
||||
name='told2'
|
||||
value={told2}
|
||||
label='Told member to clear any shelves'
|
||||
required
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './light.css';
|
||||
import { Container, Checkbox, Form, Header, Segment } from 'semantic-ui-react';
|
||||
import { Container, Checkbox, Form, Header, Segment, Table } from 'semantic-ui-react';
|
||||
import * as Datetime from 'react-datetime';
|
||||
import 'react-datetime/css/react-datetime.css';
|
||||
import moment from 'moment';
|
||||
@@ -42,28 +42,25 @@ export function AdminReportedTransactions(props) {
|
||||
};
|
||||
|
||||
let transactionsCache = false;
|
||||
let excludePayPalCache = false;
|
||||
let summaryCache = false;
|
||||
|
||||
export function AdminHistoricalTransactions(props) {
|
||||
const { token } = props;
|
||||
const [input, setInput] = useState({ month: moment() });
|
||||
const [transactions, setTransactions] = useState(transactionsCache);
|
||||
const [excludePayPal, setExcludePayPal] = useState(excludePayPalCache);
|
||||
const [summary, setSummary] = useState(summaryCache);
|
||||
const [excludePayPal, setExcludePayPal] = useState(false);
|
||||
const [excludeSnacks, setExcludeSnacks] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const handleDatetime = (v) => setInput({ ...input, month: v });
|
||||
|
||||
const handleExcludePayPal = (e, v) => {
|
||||
setExcludePayPal(v.checked);
|
||||
excludePayPalCache = v.checked;
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
const makeRequest = () => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
const month = input.month.format('YYYY-MM');
|
||||
requester('/transactions/?month=' + month, 'GET', token)
|
||||
requester('/transactions/?month=' + month + '&exclude_paypal=' + excludePayPal + '&exclude_snacks=' + excludeSnacks, 'GET', token)
|
||||
.then(res => {
|
||||
setLoading(false);
|
||||
setError(false);
|
||||
@@ -75,8 +72,37 @@ export function AdminHistoricalTransactions(props) {
|
||||
console.log(err);
|
||||
setError(true);
|
||||
});
|
||||
|
||||
requester('/transactions/summary/?month=' + month, 'GET', token)
|
||||
.then(res => {
|
||||
setLoading(false);
|
||||
setError(false);
|
||||
setSummary(res);
|
||||
summaryCache = res;
|
||||
})
|
||||
.catch(err => {
|
||||
setLoading(false);
|
||||
console.log(err);
|
||||
setError(true);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
makeRequest();
|
||||
};
|
||||
|
||||
const handleExcludePayPal = (e, v) => {
|
||||
setExcludePayPal(v.checked);
|
||||
};
|
||||
|
||||
const handleExcludeSnacks = (e, v) => {
|
||||
setExcludeSnacks(v.checked);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest();
|
||||
}, [excludePayPal, excludeSnacks]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
@@ -96,21 +122,59 @@ export function AdminHistoricalTransactions(props) {
|
||||
</Form.Button>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
{transactions && <p>Found {transactions.length} transactions.</p>}
|
||||
|
||||
{!error ?
|
||||
summary && <div>
|
||||
<Header size='small'>Summary</Header>
|
||||
|
||||
<Table basic='very'>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Category</Table.HeaderCell>
|
||||
<Table.HeaderCell>Dollar</Table.HeaderCell>
|
||||
<Table.HeaderCell>Protocoin</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{summary.map(x =>
|
||||
<Table.Row key={x.category}>
|
||||
<Table.Cell>{x.category}</Table.Cell>
|
||||
<Table.Cell>{'$ ' + x.dollar.toFixed(2)}</Table.Cell>
|
||||
<Table.Cell>{'₱ ' + x.protocoin.toFixed(2)}</Table.Cell>
|
||||
</Table.Row>
|
||||
)}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
:
|
||||
<p>Error loading summary.</p>
|
||||
}
|
||||
|
||||
<p/>
|
||||
|
||||
{!error ?
|
||||
transactions && <div>
|
||||
<p>Found {transactions.length} transactions.</p>
|
||||
{!!transactions.length &&
|
||||
<Header size='small'>{moment(transactions[0].date, 'YYYY-MM-DD').format('MMMM YYYY')} Transactions</Header>
|
||||
}
|
||||
|
||||
<Checkbox
|
||||
className='filter-option'
|
||||
label='Exclude PayPal'
|
||||
onChange={handleExcludePayPal}
|
||||
checked={excludePayPal}
|
||||
/>
|
||||
|
||||
<TransactionList transactions={transactions.filter(x => !excludePayPal || x.account_type !== 'PayPal')} />
|
||||
<Checkbox
|
||||
className='filter-option'
|
||||
label='Exclude Snacks'
|
||||
onChange={handleExcludeSnacks}
|
||||
checked={excludeSnacks}
|
||||
/>
|
||||
|
||||
<TransactionList transactions={transactions} />
|
||||
</div>
|
||||
:
|
||||
<p>Error loading transactions.</p>
|
||||
|
@@ -29,7 +29,7 @@ import { NotFound, PleaseLogin } from './Misc.js';
|
||||
import { Debug } from './Debug.js';
|
||||
import { Garden } from './Garden.js';
|
||||
import { Footer } from './Footer.js';
|
||||
import { LCARS1Display } from './Display.js';
|
||||
import { LCARS1Display, LCARS2Display } from './Display.js';
|
||||
|
||||
const APP_VERSION = 5; // TODO: automate this
|
||||
|
||||
@@ -98,7 +98,8 @@ function App() {
|
||||
right: '16px',
|
||||
buttonColorDark: '#666',
|
||||
buttonColorLight: '#aaa',
|
||||
label: '🌙',
|
||||
label: '🌓',
|
||||
autoMatchOsTheme: false,
|
||||
}
|
||||
const darkmode = new Darkmode(options);
|
||||
darkmode.showWidget();
|
||||
@@ -129,6 +130,10 @@ function App() {
|
||||
<LCARS1Display token={token} />
|
||||
</Route>
|
||||
|
||||
<Route exact path='/display/lcars2'>
|
||||
<LCARS2Display token={token} />
|
||||
</Route>
|
||||
|
||||
<Route path='/'>
|
||||
<Container>
|
||||
<div className='hero'>
|
||||
|
@@ -20,8 +20,8 @@ export function Cards(props) {
|
||||
{user.member.card_photo ?
|
||||
<p>
|
||||
<a href={staticUrl + '/' + user.member.card_photo} target='_blank'>
|
||||
Click here
|
||||
</a> to view your card image.
|
||||
View your card image.
|
||||
</a>
|
||||
</p>
|
||||
:
|
||||
<p>Upload a photo to generate a card image.</p>
|
||||
|
@@ -310,7 +310,7 @@ export function Charts(props) {
|
||||
<XAxis dataKey='date' minTickGap={10} />
|
||||
<YAxis />
|
||||
<CartesianGrid strokeDasharray='3 3'/>
|
||||
<Tooltip />
|
||||
<Tooltip labelFormatter={t => moment(t).format('YYYY-MM-DD ddd')} />
|
||||
<Legend />
|
||||
|
||||
<Bar
|
||||
|
@@ -7,6 +7,7 @@ import { apiUrl, isAdmin, getInstructor, BasicTable, requester, useIsMobile } fr
|
||||
import { NotFound } from './Misc.js';
|
||||
import { InstructorClassDetail, InstructorClassAttendance } from './InstructorClasses.js';
|
||||
import { PayPalPayNow } from './PayPal.js';
|
||||
import { PayWithProtocoin } from './Paymaster.js';
|
||||
import { tags } from './Courses.js';
|
||||
|
||||
function ClassTable(props) {
|
||||
@@ -298,6 +299,8 @@ export function ClassFeed(props) {
|
||||
:
|
||||
<p>Loading...</p>
|
||||
}
|
||||
|
||||
<p style={{ marginBottom: '30rem' }}/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -691,6 +694,20 @@ export function ClassDetail(props) {
|
||||
name={clazz.course_data.name}
|
||||
custom={JSON.stringify({ training: userTraining.id })}
|
||||
/>
|
||||
|
||||
<p/>
|
||||
|
||||
<p>Current balance: ₱ {user.member.protocoin.toFixed(2)}</p>
|
||||
|
||||
<PayWithProtocoin
|
||||
token={token} user={user} refreshUser={refreshUser}
|
||||
amount={clazz.cost}
|
||||
onSuccess={() => {
|
||||
refreshUser();
|
||||
refreshClass();
|
||||
}}
|
||||
custom={{ category: 'OnAcct', training: userTraining.id }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@@ -235,7 +235,7 @@ export function CourseDetail(props) {
|
||||
|
||||
<Table.Body>
|
||||
{course.sessions.length ?
|
||||
course.sessions.sort((a, b) => a.datetime < b.datetime ? 1 : -1).slice(0,10).map(x =>
|
||||
course.sessions.sort((a, b) => a.datetime < b.datetime ? 1 : -1).map(x =>
|
||||
<Table.Row key={x.id} active={x.datetime < now || x.is_cancelled}>
|
||||
<Table.Cell>
|
||||
<Link to={'/classes/'+x.id}>
|
||||
|
@@ -26,6 +26,10 @@ export function Debug(props) {
|
||||
|
||||
<p><Link to='/usage/trotec'>Trotec Usage</Link></p>
|
||||
|
||||
<p><Link to='/display/lcars1'>LCARS1 Display</Link></p>
|
||||
|
||||
<p><Link to='/display/lcars2'>LCARS2 Display</Link></p>
|
||||
|
||||
|
||||
</Container>
|
||||
);
|
||||
|
@@ -34,7 +34,17 @@ export function LCARS1Display(props) {
|
||||
</p>
|
||||
}
|
||||
|
||||
<div></div>
|
||||
<div className='display-scores'>
|
||||
<DisplayScores />
|
||||
</div>
|
||||
|
||||
<div className='display-scores'>
|
||||
<DisplayMonthlyScores />
|
||||
</div>
|
||||
|
||||
<div className='display-scores'>
|
||||
<DisplayHosting />
|
||||
</div>
|
||||
|
||||
<div className='display-usage'>
|
||||
<DisplayUsage token={token} name={'trotec'} />
|
||||
@@ -44,6 +54,43 @@ export function LCARS1Display(props) {
|
||||
);
|
||||
};
|
||||
|
||||
export function LCARS2Display(props) {
|
||||
const { token } = props;
|
||||
const [fullElement, setFullElement] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
const goFullScreen = () => {
|
||||
if ('wakeLock' in navigator) {
|
||||
navigator.wakeLock.request('screen');
|
||||
}
|
||||
|
||||
ref.current.requestFullscreen({ navigationUI: 'hide' }).then(() => {
|
||||
setFullElement(true);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className='display' ref={ref}>
|
||||
|
||||
{!fullElement &&
|
||||
<p>
|
||||
<Button onClick={goFullScreen}>Fullscreen</Button>
|
||||
</p>
|
||||
}
|
||||
|
||||
<div className='display-scores'>
|
||||
<DisplayScores />
|
||||
</div>
|
||||
|
||||
<div className='display-scores'>
|
||||
<DisplayHosting />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export function DisplayUsage(props) {
|
||||
const { token, name } = props;
|
||||
const title = deviceNames[name].title;
|
||||
@@ -67,21 +114,128 @@ export function DisplayUsage(props) {
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const showUsage = usage && usage.track.username === usage.username;
|
||||
const inUse = usage && moment().unix() - usage.track.time <= 60;
|
||||
const showUsage = usage && inUse && usage.track.username === usage.username;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header size='large'>Trotec Usage</Header>
|
||||
|
||||
{showUsage ?
|
||||
<TrotecUsage usage={usage} />
|
||||
:
|
||||
<>
|
||||
<Header size='medium'>Trotec Usage</Header>
|
||||
|
||||
<p className='stat'>
|
||||
Waiting for job
|
||||
</p>
|
||||
</>
|
||||
<p className='stat'>
|
||||
Waiting for job
|
||||
</p>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export function DisplayScores(props) {
|
||||
const { token, name } = props;
|
||||
const [scores, setScores] = useState(false);
|
||||
|
||||
const getScores = () => {
|
||||
requester('/pinball/high_scores/', 'GET')
|
||||
.then(res => {
|
||||
setScores(res);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
setScores(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getScores();
|
||||
const interval = setInterval(getScores, 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header size='large'>Pinball High Scores</Header>
|
||||
|
||||
{scores && scores.slice(0, 5).map((x, i) =>
|
||||
<div key={i}>
|
||||
<Header size='medium'>#{i+1} — {x.name}. {i === 0 ? '👑' : ''}</Header>
|
||||
<p>{x.score.toLocaleString()}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export function DisplayMonthlyScores(props) {
|
||||
const { token, name } = props;
|
||||
const [scores, setScores] = useState(false);
|
||||
|
||||
const getScores = () => {
|
||||
requester('/pinball/monthly_high_scores/', 'GET')
|
||||
.then(res => {
|
||||
setScores(res);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
setScores(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getScores();
|
||||
const interval = setInterval(getScores, 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header size='large'>Monthly High Scores</Header>
|
||||
|
||||
{scores && scores.slice(0, 5).map((x, i) =>
|
||||
<div key={i}>
|
||||
<Header size='medium'>#{i+1} — {x.name}. {i === 0 ? '🧙' : ''}</Header>
|
||||
<p>{x.score.toLocaleString()}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export function DisplayHosting(props) {
|
||||
const { token, name } = props;
|
||||
const [scores, setScores] = useState(false);
|
||||
|
||||
const getScores = () => {
|
||||
requester('/hosting/high_scores/', 'GET')
|
||||
.then(res => {
|
||||
setScores(res);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
setScores(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getScores();
|
||||
const interval = setInterval(getScores, 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header size='large'>Most Host</Header>
|
||||
|
||||
{scores && scores.slice(0, 5).map((x, i) =>
|
||||
<div key={i}>
|
||||
<Header size='medium'>#{i+1} — {x.name}. {i === 0 ? <img className='toast' src='/toast.png' /> : ''}</Header>
|
||||
<p>{x.hours.toFixed(2)} hours</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -49,9 +49,9 @@ export const Footer = () => {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Click here
|
||||
View the source code and license on GitHub.
|
||||
</a>{' '}
|
||||
to view the source code and license.
|
||||
|
||||
</p>
|
||||
|
||||
<p>
|
||||
@@ -97,7 +97,7 @@ export const Footer = () => {
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>© 2020 Calgary Protospace Ltd.</p>
|
||||
<p>© 2020-{new Date().getFullYear()} Calgary Protospace Ltd.</p>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
|
@@ -22,6 +22,11 @@ function MemberInfo(props) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{member.protocoin < 0 && <Message error>
|
||||
<Message.Header>Your Protocoin balance is negative!</Message.Header>
|
||||
<p>Visit the <Link to='/paymaster'>Paymaster</Link> page or pay a Director to buy Protocoin.</p>
|
||||
</Message>}
|
||||
|
||||
<Grid stackable>
|
||||
<Grid.Column width={5}>
|
||||
<Image
|
||||
@@ -65,8 +70,8 @@ function MemberInfo(props) {
|
||||
<Message.Header>Welcome, new member!</Message.Header>
|
||||
<p>
|
||||
<a href={staticUrl + '/' + member.member_forms} target='_blank'>
|
||||
Click here
|
||||
</a> to view your application forms.
|
||||
View your application forms.
|
||||
</a>
|
||||
</p>
|
||||
</Message>}
|
||||
|
||||
@@ -89,8 +94,6 @@ function MemberInfo(props) {
|
||||
<QRCode value={siteUrl + 'subscribe?monthly_fees=' + user.member.monthly_fees + '&id=' + user.member.id} />
|
||||
</React.Fragment>}
|
||||
|
||||
<Header size='medium'>Latest Training</Header>
|
||||
|
||||
{unpaidTraining.map(x =>
|
||||
<Message warning>
|
||||
<Message.Header>Please pay your course fee!</Message.Header>
|
||||
@@ -98,6 +101,12 @@ function MemberInfo(props) {
|
||||
</Message>
|
||||
)}
|
||||
|
||||
<Header size='medium'>Latest Training</Header>
|
||||
|
||||
{!member.orientation_date && <p>
|
||||
⚠️ You need to attend a <Link to={'/courses/249/'}>New Member Orientation</Link> to use any tool larger than a screwdriver.
|
||||
</p>}
|
||||
|
||||
<BasicTable>
|
||||
<Table.Body>
|
||||
{lastTrain.length ?
|
||||
@@ -110,7 +119,7 @@ function MemberInfo(props) {
|
||||
</Table.Row>
|
||||
)
|
||||
:
|
||||
<Table.Row><Table.Cell>None, please sign up for an <Link to={'/courses/249/'}>Orientation</Link></Table.Cell></Table.Row>
|
||||
<Table.Row><Table.Cell>None</Table.Cell></Table.Row>
|
||||
}
|
||||
{user.training.length > 3 &&
|
||||
<Table.Row><Table.Cell>
|
||||
@@ -231,9 +240,15 @@ export function Home(props) {
|
||||
const getTrackAgo = (x) => stats && stats.track && stats.track[x] ? moment.unix(stats.track[x]['time']).tz('America/Edmonton').fromNow() : '';
|
||||
const getTrackName = (x) => stats && stats.track && stats.track[x] && stats.track[x]['first_name'] ? stats.track[x]['first_name'] : 'Unknown';
|
||||
|
||||
const alarmStat = () => stats && stats.alarm && moment().unix() - stats.alarm['time'] < 300 ? stats.alarm['data'] < 270 ? 'Armed' : 'Disarmed' : 'Unknown';
|
||||
//const alarmStat = () => stats && stats.alarm && moment().unix() - stats.alarm['time'] < 300 ? stats.alarm['data'] < 270 ? 'Armed' : 'Disarmed' : 'Unknown';
|
||||
const alarmStat = () => 'Unknown';
|
||||
|
||||
const doorOpenStat = () => alarmStat() === 'Disarmed' && stats.alarm['data'] > 360 ? ', door open' : '';
|
||||
//const doorOpenStat = () => alarmStat() === 'Disarmed' && stats.alarm['data'] > 360 ? ', door open' : '';
|
||||
const doorOpenStat = () => '';
|
||||
|
||||
const closedStat = (x) => stats && stats.closing ? moment().unix() > stats.closing['time'] ? 'Closed' : 'Open until ' + stats.closing['time_str'] : 'Unknown';
|
||||
|
||||
const printer3dStat = (x) => stats && stats.printer3d && stats.printer3d[x] ? stats.printer3d[x].state === 'Printing' ? 'Printing (' + stats.printer3d[x].progress + '%)' : stats.printer3d[x].state : 'Unknown';
|
||||
|
||||
const show_signup = stats?.at_protospace;
|
||||
|
||||
@@ -261,8 +276,8 @@ export function Home(props) {
|
||||
{user?.member?.set_details !== false &&
|
||||
<Segment>
|
||||
<Header size='medium'>Quick Links</Header>
|
||||
<p><a href='http://protospace.ca/' target='_blank' rel='noopener noreferrer'>Main Website</a></p>
|
||||
<p><a href='http://wiki.protospace.ca/Welcome_to_Protospace' target='_blank' rel='noopener noreferrer'>Protospace Wiki</a> — <Link to='/auth/wiki'>[register]</Link></p>
|
||||
<p><a href='https://protospace.ca/' target='_blank' rel='noopener noreferrer'>Main Website</a></p>
|
||||
<p><a href='https://wiki.protospace.ca/Welcome_to_Protospace' target='_blank' rel='noopener noreferrer'>Protospace Wiki</a> — <Link to='/auth/wiki'>[register]</Link></p>
|
||||
<p><a href='https://forum.protospace.ca' target='_blank' rel='noopener noreferrer'>Forum (Spacebar)</a> — <Link to='/auth/discourse'>[register]</Link></p>
|
||||
{!!user && <p><a href='https://drive.google.com/drive/folders/0By-vvp6fxFekfmU1cmdxaVRlaldiYXVyTE9rRnNVNjhkc3FjdkFIbjBwQkZ3MVVQX2Ezc3M?resourcekey=0-qVLjcYr8ZCmLypdINk2svg' target='_blank' rel='noopener noreferrer'>Google Drive</a></p>}
|
||||
{!!user && isAdmin(user) && <p><a href='https://estancia.hippocmms.ca/' target='_blank' rel='noopener noreferrer'>Property Management Portal</a></p>}
|
||||
@@ -348,7 +363,33 @@ export function Home(props) {
|
||||
} trigger={<a>[more]</a>} />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Media computer: {getTrackStat('PROTOGRAPH1')} <Popup content={
|
||||
<React.Fragment>
|
||||
<p>
|
||||
Last use:<br />
|
||||
{getTrackLast('PROTOGRAPH1')}<br />
|
||||
{getTrackAgo('PROTOGRAPH1')}<br />
|
||||
by {getTrackName('PROTOGRAPH1')}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Last print:<br />
|
||||
{getTrackLast('LASTLARGEPRINT')}<br />
|
||||
{getTrackAgo('LASTLARGEPRINT')}<br />
|
||||
by {getTrackName('LASTLARGEPRINT')}
|
||||
</p>
|
||||
</React.Fragment>
|
||||
} trigger={<a>[more]</a>} />
|
||||
</p>
|
||||
|
||||
<p>ORD2 printer: {printer3dStat('ord2')}</p>
|
||||
|
||||
<p>ORD3 printer: {printer3dStat('ord3')}</p>
|
||||
|
||||
{user && <p>Alarm status: {alarmStat()}{doorOpenStat()}</p>}
|
||||
|
||||
{user && <p>Hosting status: {closedStat()}</p>}
|
||||
</div>
|
||||
|
||||
<SignForm token={token} />
|
||||
|
@@ -230,7 +230,7 @@ export function Members(props) {
|
||||
</Item.Header>
|
||||
{sort === 'pinball_score' ?
|
||||
<>
|
||||
<Item.Description>Score: {x.member.pinball_score || 'Unknown'}</Item.Description>
|
||||
<Item.Description>Score: {x.member.pinball_score.toLocaleString() || 'Unknown'}</Item.Description>
|
||||
<Item.Description>Rank: {i === 0 ? 'Pinball Wizard' : 'Not the Pinball Wizard'}</Item.Description>
|
||||
</>
|
||||
:
|
||||
|
@@ -6,6 +6,46 @@ import { PayPalPayNow, PayPalSubscribe } from './PayPal.js';
|
||||
import { MembersDropdown } from './Members.js';
|
||||
import { requester } from './utils.js';
|
||||
|
||||
export function PayWithProtocoin(props) {
|
||||
const { token, user, refreshUser, amount, onSuccess, custom } = props;
|
||||
const member = user.member;
|
||||
const [error, setError] = useState({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
if (loading) return;
|
||||
setSuccess(false);
|
||||
setLoading(true);
|
||||
|
||||
const data = { amount: amount, ...custom, balance: member.protocoin };
|
||||
requester('/protocoin/spend_request/', 'POST', token, data)
|
||||
.then(res => {
|
||||
setLoading(false);
|
||||
setSuccess(true);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
setError({});
|
||||
refreshUser();
|
||||
})
|
||||
.catch(err => {
|
||||
setLoading(false);
|
||||
console.log(err);
|
||||
setError(err.data);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Button disabled={!amount} color='green' loading={loading} error={error.amount}>
|
||||
Pay with Protocoin
|
||||
</Form.Button>
|
||||
{success && <div>Success!</div>}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export function SendProtocoin(props) {
|
||||
const { token, user, refreshUser } = props;
|
||||
const member = user.member;
|
||||
@@ -76,10 +116,10 @@ export function Paymaster(props) {
|
||||
const { token, user, refreshUser } = props;
|
||||
const [pop, setPop] = useState('20.00');
|
||||
const [locker, setLocker] = useState('5.00');
|
||||
const [consumables, setConsumables] = useState('20.00');
|
||||
const [consumables, setConsumables] = useState('');
|
||||
const [buyProtocoin, setBuyProtocoin] = useState('10.00');
|
||||
const [consumablesMemo, setConsumablesMemo] = useState('');
|
||||
const [donate, setDonate] = useState('20.00');
|
||||
const [donate, setDonate] = useState('');
|
||||
const [memo, setMemo] = useState('');
|
||||
|
||||
const monthly_fees = user.member.monthly_fees || 55;
|
||||
@@ -156,12 +196,12 @@ export function Paymaster(props) {
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
|
||||
<Header size='medium'>Consumables</Header>
|
||||
|
||||
<p>Pay for materials you use (ie. welding gas, 3D printing, blades, etc).</p>
|
||||
|
||||
<Grid stackable padded columns={1}>
|
||||
<Grid stackable columns={2}>
|
||||
<Grid.Column>
|
||||
<Header size='medium'>Consumables</Header>
|
||||
|
||||
<p>Pay for materials you use (ie. welding gas, 3D printing, etc).</p>
|
||||
|
||||
Custom amount:
|
||||
|
||||
<div className='pay-custom'>
|
||||
@@ -188,13 +228,21 @@ export function Paymaster(props) {
|
||||
name='Protospace Consumables'
|
||||
custom={JSON.stringify({ category: 'Consumables', member: user.member.id, memo: consumablesMemo })}
|
||||
/>
|
||||
|
||||
<p/>
|
||||
|
||||
<PayWithProtocoin
|
||||
token={token} user={user} refreshUser={refreshUser}
|
||||
amount={consumables}
|
||||
onSuccess={() => setConsumables('')}
|
||||
custom={{ category: 'Consumables', memo: consumablesMemo }}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
|
||||
<Header size='medium'>Donate</Header>
|
||||
|
||||
<Grid stackable padded columns={1}>
|
||||
<Grid.Column>
|
||||
<Header size='medium'>Donate</Header>
|
||||
|
||||
<p>Donation of any amount to Protospace.</p>
|
||||
|
||||
Custom amount:
|
||||
|
||||
<div className='pay-custom'>
|
||||
@@ -221,6 +269,15 @@ export function Paymaster(props) {
|
||||
name='Protospace Donation'
|
||||
custom={JSON.stringify({ category: 'Donation', member: user.member.id, memo: memo })}
|
||||
/>
|
||||
|
||||
<p/>
|
||||
|
||||
<PayWithProtocoin
|
||||
token={token} user={user} refreshUser={refreshUser}
|
||||
amount={donate}
|
||||
onSuccess={() => setDonate('')}
|
||||
custom={{ category: 'Donation', memo: memo }}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
|
||||
|
@@ -211,66 +211,6 @@ function EditTransaction(props) {
|
||||
);
|
||||
};
|
||||
|
||||
function ReportTransaction(props) {
|
||||
const { transaction, token, refreshUser } = props;
|
||||
const [input, setInput] = useState(transaction);
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const { id } = useParams();
|
||||
|
||||
const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value });
|
||||
const handleChange = (e) => handleValues(e, e.currentTarget);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
setSuccess(false);
|
||||
requester('/transactions/'+id+'/report/', 'POST', token, input)
|
||||
.then(res => {
|
||||
setLoading(false);
|
||||
setSuccess(true);
|
||||
setError(false);
|
||||
if (refreshUser) {
|
||||
refreshUser();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
setLoading(false);
|
||||
console.log(err);
|
||||
setError(err.data);
|
||||
});
|
||||
};
|
||||
|
||||
const makeProps = (name) => ({
|
||||
name: name,
|
||||
onChange: handleChange,
|
||||
value: input[name] || '',
|
||||
error: error[name],
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header size='medium'>Report Transaction</Header>
|
||||
|
||||
<p>If this transaction was made in error or there is anything incorrect about it, please report it using this form.</p>
|
||||
<p>A staff member will review the report as soon as possible.</p>
|
||||
<p>Follow up with <a href='mailto:directors@protospace.ca' target='_blank' rel='noopener noreferrer'>directors@protospace.ca</a>.</p>
|
||||
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.TextArea
|
||||
label='Reason'
|
||||
{...makeProps('report_memo')}
|
||||
/>
|
||||
|
||||
<Form.Button loading={loading} error={error.non_field_errors}>
|
||||
Submit Report
|
||||
</Form.Button>
|
||||
{success && <div>Success!</div>}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function TransactionList(props) {
|
||||
const { transactions, noMember, noCategory } = props;
|
||||
@@ -474,7 +414,11 @@ export function TransactionDetail(props) {
|
||||
</Segment>
|
||||
:
|
||||
<Segment padded>
|
||||
<ReportTransaction transaction={transaction} setTransaction={setTransaction} {...props} />
|
||||
<Header size='medium'>Report Transaction</Header>
|
||||
|
||||
<p>If there's anything wrong with this transaction or it was made in error please email the Protospace Directors:</p>
|
||||
<p><a href='mailto:directors@protospace.ca' target='_blank' rel='noopener noreferrer'>directors@protospace.ca</a></p>
|
||||
<p>Please include a link to this transaction and any relevant details.</p>
|
||||
</Segment>
|
||||
}
|
||||
</Grid.Column>
|
||||
|
@@ -157,6 +157,11 @@ body {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.adminvetting .ui.button {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.interest .ui.button {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
@@ -184,15 +189,34 @@ body {
|
||||
height: 100vh;
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-size: 1em;
|
||||
font-size: 1.75em;
|
||||
}
|
||||
|
||||
.display-usage {
|
||||
border: 1px solid white;
|
||||
padding: 0.5em;
|
||||
align-self: flex-end;
|
||||
width: 240px;
|
||||
height: 383px;
|
||||
width: 25vw;
|
||||
height: 75vh;
|
||||
}
|
||||
|
||||
.display-scores {
|
||||
border: 1px solid white;
|
||||
padding: 0.5em;
|
||||
align-self: flex-end;
|
||||
width: 25vw;
|
||||
height: 75vh;
|
||||
}
|
||||
|
||||
.display-scores p {
|
||||
font-size: 1.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.display .display-scores .toast {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.usage {
|
||||
@@ -222,6 +246,10 @@ body {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.footer {
|
||||
margin-top: -20rem;
|
||||
|
@@ -30,6 +30,8 @@ export const statusColor = {
|
||||
'Due': 'yellow',
|
||||
'Overdue': 'red',
|
||||
'Former Member': 'black',
|
||||
'Paused Member': 'black',
|
||||
'Expired Member': 'black',
|
||||
};
|
||||
|
||||
export const BasicTable = (props) => (
|
||||
|
Reference in New Issue
Block a user