Merge branch 'master' into storage_space

This commit is contained in:
2023-05-29 12:16:51 -06:00
36 changed files with 1407 additions and 210 deletions

BIN
webclient/public/toast.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -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} />

View File

@@ -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

View File

@@ -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>

View File

@@ -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'>

View File

@@ -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>

View File

@@ -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

View File

@@ -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: &thinsp;{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>

View File

@@ -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}>

View File

@@ -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>
);

View File

@@ -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>
)}
</>
);
};

View File

@@ -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>
);

View File

@@ -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} />

View File

@@ -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>
</>
:

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -30,6 +30,8 @@ export const statusColor = {
'Due': 'yellow',
'Overdue': 'red',
'Former Member': 'black',
'Paused Member': 'black',
'Expired Member': 'black',
};
export const BasicTable = (props) => (