2023-05-30 19:35:07 +00:00
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
import { Link, useParams, useHistory } from 'react-router-dom';
|
|
|
|
import './light.css';
|
|
|
|
import { MembersDropdown } from './Members.js';
|
2023-06-23 00:58:32 +00:00
|
|
|
import { statusColor, isAdmin, BasicTable, requester, useIsMobile } from './utils.js';
|
2023-06-23 00:32:26 +00:00
|
|
|
import { Button, Checkbox, Container, Form, Grid, Header, Icon, Input, Message, Segment, Table } from 'semantic-ui-react';
|
2023-05-30 19:35:07 +00:00
|
|
|
|
|
|
|
export function StorageEditor(props) {
|
|
|
|
const { token, input, setInput, error } = props;
|
|
|
|
|
|
|
|
const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value });
|
|
|
|
const handleChange = (e) => handleValues(e, e.currentTarget);
|
|
|
|
|
|
|
|
const makeProps = (name) => ({
|
|
|
|
name: name,
|
|
|
|
onChange: handleChange,
|
|
|
|
value: input[name] || '',
|
|
|
|
error: error[name],
|
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className='transaction-editor'>
|
|
|
|
<Form.Field error={error.member_id}>
|
|
|
|
<label>Owner (search)</label>
|
|
|
|
<MembersDropdown
|
|
|
|
token={token}
|
|
|
|
{...makeProps('member_id')}
|
|
|
|
onChange={handleValues}
|
|
|
|
initial={input.member_name}
|
2023-05-30 20:36:08 +00:00
|
|
|
autofocus={!input.member_name}
|
2023-05-30 19:35:07 +00:00
|
|
|
/>
|
|
|
|
</Form.Field>
|
|
|
|
|
|
|
|
<Form.Input
|
|
|
|
label='Memo'
|
|
|
|
fluid
|
|
|
|
{...makeProps('memo')}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
function EditStorage(props) {
|
|
|
|
const { storage, setStorage, token, refreshUser } = props;
|
|
|
|
const [input, setInput] = useState(storage);
|
|
|
|
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 };
|
|
|
|
return requester('/storage/'+id+'/', 'PUT', token, data)
|
|
|
|
.then(res => {
|
|
|
|
setLoading(false);
|
|
|
|
setSuccess(true);
|
|
|
|
setError(false);
|
|
|
|
setInput(res);
|
|
|
|
setStorage(res);
|
|
|
|
})
|
|
|
|
.catch(err => {
|
|
|
|
setLoading(false);
|
|
|
|
console.log(err);
|
|
|
|
setError(err.data);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
2023-05-30 21:10:05 +00:00
|
|
|
<Header size='medium'>Edit Storage {storage.shelf_id}</Header>
|
2023-05-30 19:35:07 +00:00
|
|
|
|
|
|
|
<Form onSubmit={handleSubmit}>
|
|
|
|
<StorageEditor token={token} input={input} setInput={setInput} error={error} />
|
|
|
|
|
|
|
|
<Form.Group widths='equal'>
|
|
|
|
<Form.Button loading={loading} error={error.non_field_errors}>
|
|
|
|
Save
|
|
|
|
</Form.Button>
|
|
|
|
</Form.Group>
|
|
|
|
{success && <div>Success!</div>}
|
|
|
|
</Form>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
function StorageTable(props) {
|
|
|
|
const { storage, user } = props;
|
|
|
|
|
2023-05-30 23:52:30 +00:00
|
|
|
const locations = {
|
|
|
|
member_shelves: 'Member Shelves',
|
|
|
|
lockers: 'Lockers',
|
|
|
|
large_project_storage: 'Large Project Storage',
|
|
|
|
};
|
|
|
|
|
2023-05-30 19:35:07 +00:00
|
|
|
return (
|
|
|
|
<BasicTable>
|
|
|
|
<Table.Body>
|
|
|
|
<Table.Row>
|
|
|
|
<Table.Cell>Shelf ID:</Table.Cell>
|
|
|
|
<Table.Cell>{storage.shelf_id}</Table.Cell>
|
|
|
|
</Table.Row>
|
|
|
|
<Table.Row>
|
|
|
|
<Table.Cell>Owner:</Table.Cell>
|
2023-05-30 23:52:30 +00:00
|
|
|
{storage.member_id ?
|
2023-05-30 19:35:07 +00:00
|
|
|
<Table.Cell>
|
|
|
|
<Link to={'/members/'+storage.member_id}>
|
|
|
|
{storage.member_name}
|
|
|
|
</Link>
|
|
|
|
</Table.Cell>
|
|
|
|
:
|
2023-05-30 23:52:30 +00:00
|
|
|
<Table.Cell>None</Table.Cell>
|
2023-05-30 19:35:07 +00:00
|
|
|
}
|
|
|
|
</Table.Row>
|
|
|
|
<Table.Row>
|
|
|
|
<Table.Cell>Location:</Table.Cell>
|
2023-05-30 23:52:30 +00:00
|
|
|
<Table.Cell>
|
|
|
|
{locations[storage.location]}
|
|
|
|
<p>
|
|
|
|
Aisle {storage.shelf_id[0]} <br/>
|
|
|
|
Column {storage.shelf_id[1]} <br/>
|
|
|
|
Row {storage.shelf_id[2]}
|
|
|
|
</p>
|
|
|
|
</Table.Cell>
|
2023-05-30 19:35:07 +00:00
|
|
|
</Table.Row>
|
|
|
|
<Table.Row>
|
|
|
|
<Table.Cell>Memo:</Table.Cell>
|
2023-05-31 02:47:58 +00:00
|
|
|
<Table.Cell>{storage.memo || 'None'}</Table.Cell>
|
2023-05-30 19:35:07 +00:00
|
|
|
</Table.Row>
|
|
|
|
</Table.Body>
|
|
|
|
</BasicTable>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function StorageDetail(props) {
|
|
|
|
const { token, user } = props;
|
|
|
|
const [storage, setStorage] = useState(false);
|
|
|
|
const [error, setError] = useState(false);
|
|
|
|
const { id } = useParams();
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
requester('/storage/' + id + '/', 'GET', token)
|
|
|
|
.then(res => {
|
|
|
|
setStorage(res);
|
|
|
|
setError(false);
|
|
|
|
})
|
|
|
|
.catch(err => {
|
|
|
|
console.log(err);
|
|
|
|
setError(true);
|
|
|
|
});
|
|
|
|
}, [id]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Container>
|
|
|
|
{!error ?
|
|
|
|
storage ?
|
|
|
|
<div>
|
|
|
|
<Header size='large'>Storage Location</Header>
|
|
|
|
|
2023-06-03 18:56:01 +00:00
|
|
|
<p><Link to={'/storage'}>View the list of all storage locations.</Link></p>
|
|
|
|
|
2023-05-30 19:35:07 +00:00
|
|
|
<Grid stackable columns={2}>
|
|
|
|
<Grid.Column width={6}>
|
|
|
|
<StorageTable user={user} storage={storage} />
|
|
|
|
</Grid.Column>
|
|
|
|
|
|
|
|
<Grid.Column width={10}>
|
|
|
|
{isAdmin(user) ?
|
|
|
|
<Segment padded>
|
|
|
|
<EditStorage storage={storage} setStorage={setStorage} token={token} {...props} />
|
|
|
|
</Segment>
|
|
|
|
:
|
|
|
|
<Segment padded>
|
|
|
|
<Header size='medium'>Report Storage</Header>
|
|
|
|
|
|
|
|
<p>If there's anything wrong with this storage location 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 storage location and any relevant details.</p>
|
|
|
|
</Segment>
|
|
|
|
}
|
|
|
|
</Grid.Column>
|
|
|
|
</Grid>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
:
|
|
|
|
<p>Loading...</p>
|
|
|
|
:
|
|
|
|
<p>Error loading.</p>
|
|
|
|
}
|
|
|
|
</Container>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2023-06-03 18:56:01 +00:00
|
|
|
export function StorageButton(props) {
|
|
|
|
const { storage } = props;
|
|
|
|
const history = useHistory();
|
|
|
|
|
|
|
|
const buttonColors = {
|
|
|
|
member_shelves: 'grey',
|
|
|
|
lockers: 'blue',
|
|
|
|
large_project_storage: 'brown',
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleStorageButton = (e, id) => {
|
|
|
|
e.preventDefault();
|
|
|
|
history.push('/storage/' + id);
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Button
|
|
|
|
className='storage-button'
|
|
|
|
onClick={(e) => handleStorageButton(e, storage.id)}
|
|
|
|
size='tiny'
|
|
|
|
color={buttonColors[storage.location]}
|
|
|
|
>
|
|
|
|
{storage.shelf_id}
|
|
|
|
</Button>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2023-06-23 00:00:30 +00:00
|
|
|
let storageSearchCache = '';
|
|
|
|
|
|
|
|
export function StorageSearch(props) {
|
|
|
|
const { setSearch } = props;
|
|
|
|
const [input, setInput] = useState(storageSearchCache);
|
|
|
|
|
|
|
|
const handleChange = (event) => {
|
|
|
|
const q = event.target.value.toUpperCase();
|
|
|
|
setInput(q);
|
|
|
|
setSearch(q);
|
|
|
|
storageSearchCache = q;
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<Input icon='search'
|
|
|
|
placeholder='Search...'
|
|
|
|
value={input}
|
|
|
|
onChange={handleChange}
|
|
|
|
aria-label='search products'
|
|
|
|
style={{ marginRight: '0.5rem' }}
|
|
|
|
maxLength={3}
|
|
|
|
/>
|
|
|
|
|
|
|
|
{input.length ?
|
|
|
|
<Button
|
|
|
|
content='Clear'
|
|
|
|
onClick={() => {
|
|
|
|
setInput('');
|
|
|
|
setSearch('');
|
|
|
|
storageSearchCache = '';
|
|
|
|
}}
|
|
|
|
/> : ''
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
let storageListCache = false;
|
2023-06-23 00:32:26 +00:00
|
|
|
let showEmptyCache = false;
|
|
|
|
let showMemodCache = false;
|
|
|
|
let showServedCache = false;
|
2023-06-23 00:58:32 +00:00
|
|
|
let showExpiredCache = false;
|
2023-06-23 00:00:30 +00:00
|
|
|
|
2023-05-30 19:35:07 +00:00
|
|
|
export function StorageList(props) {
|
|
|
|
const { token } = props;
|
2023-06-23 00:00:30 +00:00
|
|
|
const [storageList, setStorageList] = useState(storageListCache);
|
|
|
|
const [search, setSearch] = useState(storageSearchCache);
|
2023-06-23 00:32:26 +00:00
|
|
|
const [showEmpty, setShowEmpty] = useState(showEmptyCache);
|
|
|
|
const [showMemod, setShowMemod] = useState(showMemodCache);
|
|
|
|
const [showServed, setShowServed] = useState(showServedCache);
|
2023-06-23 00:58:32 +00:00
|
|
|
const [showExpired, setShowExpired] = useState(showExpiredCache);
|
2023-05-30 19:35:07 +00:00
|
|
|
const [error, setError] = useState(false);
|
2023-06-03 18:56:01 +00:00
|
|
|
const isMobile = useIsMobile();
|
2023-05-30 19:35:07 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
requester('/storage/', 'GET', token)
|
|
|
|
.then(res => {
|
|
|
|
setStorageList(res.results);
|
2023-06-23 00:00:30 +00:00
|
|
|
storageListCache = res.results;
|
2023-05-30 19:35:07 +00:00
|
|
|
setError(false);
|
|
|
|
})
|
|
|
|
.catch(err => {
|
|
|
|
console.log(err);
|
|
|
|
setError(true);
|
|
|
|
});
|
|
|
|
}, []);
|
|
|
|
|
2023-06-23 00:00:30 +00:00
|
|
|
const filterStorage = (x) => {
|
|
|
|
if (search.length && !x.shelf_id.startsWith(search)) {
|
|
|
|
return false;
|
2023-06-23 00:32:26 +00:00
|
|
|
} else if (showEmpty && x.member_name) {
|
|
|
|
return false;
|
|
|
|
} else if (showMemod && !x.memo) {
|
|
|
|
return false;
|
|
|
|
} else if (showServed && !x.memo.toLowerCase().includes('served')) {
|
|
|
|
return false;
|
2023-06-23 00:58:32 +00:00
|
|
|
} else if (showExpired && !x.member_paused) {
|
|
|
|
return false;
|
2023-06-23 00:00:30 +00:00
|
|
|
} else {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-06-23 00:58:32 +00:00
|
|
|
const sortStorage = (a, b) => {
|
|
|
|
if (showExpired) {
|
|
|
|
return a.member_paused !== b.member_paused ? a.member_paused < b.member_paused ? -1 : 1 : 0;
|
|
|
|
} else {
|
|
|
|
return a.shelf_id !== b.shelf_id ? a.shelf_id < b.shelf_id ? -1 : 1 : 0;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-06-23 00:32:26 +00:00
|
|
|
const handleShowEmpty = (e, v) => {
|
|
|
|
setShowEmpty(v.checked);
|
|
|
|
showEmptyCache = v.checked;
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleShowMemod = (e, v) => {
|
|
|
|
setShowMemod(v.checked);
|
|
|
|
showMemodCache = v.checked;
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleShowServed = (e, v) => {
|
|
|
|
setShowServed(v.checked);
|
|
|
|
showServedCache = v.checked;
|
|
|
|
};
|
|
|
|
|
2023-06-23 00:58:32 +00:00
|
|
|
const handleShowExpired = (e, v) => {
|
|
|
|
setShowExpired(v.checked);
|
|
|
|
showExpiredCache = v.checked;
|
|
|
|
};
|
|
|
|
|
|
|
|
const numResults = storageList ? storageList.filter(filterStorage).length : 0;
|
|
|
|
|
2023-05-30 19:35:07 +00:00
|
|
|
return (
|
|
|
|
<div>
|
2023-06-03 18:56:01 +00:00
|
|
|
<p>
|
|
|
|
<Icon name='circle' color='grey' /> Member shelf <br/>
|
|
|
|
<Icon name='circle' color='blue' /> Locker <br/>
|
|
|
|
<Icon name='circle' color='brown' /> Large project storage
|
|
|
|
</p>
|
2023-06-23 00:00:30 +00:00
|
|
|
|
2023-06-23 00:32:26 +00:00
|
|
|
<p>
|
|
|
|
<StorageSearch setSearch={setSearch} />
|
|
|
|
</p>
|
|
|
|
|
|
|
|
<p>
|
|
|
|
<Checkbox
|
|
|
|
className='filter-option'
|
|
|
|
label='Show Empty'
|
|
|
|
onChange={handleShowEmpty}
|
|
|
|
checked={showEmpty}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<Checkbox
|
|
|
|
className='filter-option'
|
|
|
|
label={'Show Memo\'d'}
|
|
|
|
onChange={handleShowMemod}
|
|
|
|
checked={showMemod}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<Checkbox
|
|
|
|
className='filter-option'
|
|
|
|
label='Show Served'
|
|
|
|
onChange={handleShowServed}
|
|
|
|
checked={showServed}
|
|
|
|
/>
|
2023-06-23 00:58:32 +00:00
|
|
|
|
|
|
|
<Checkbox
|
|
|
|
className='filter-option'
|
|
|
|
label='Show Expired'
|
|
|
|
onChange={handleShowExpired}
|
|
|
|
checked={showExpired}
|
|
|
|
/>
|
2023-06-23 00:32:26 +00:00
|
|
|
</p>
|
2023-06-23 00:00:30 +00:00
|
|
|
|
2023-05-30 19:35:07 +00:00
|
|
|
{!error ?
|
|
|
|
storageList ?
|
2023-06-23 00:32:26 +00:00
|
|
|
<>
|
2023-06-23 00:58:32 +00:00
|
|
|
<p>{numResults} result{numResults === 1 ? '' : 's'}{showExpired && ', ordered by expiry'}:</p>
|
2023-06-23 00:32:26 +00:00
|
|
|
|
|
|
|
<Table basic='very'>
|
|
|
|
{!isMobile && <Table.Header>
|
|
|
|
<Table.Row>
|
|
|
|
<Table.HeaderCell>Shelf ID</Table.HeaderCell>
|
|
|
|
<Table.HeaderCell>Owner</Table.HeaderCell>
|
2023-06-23 00:58:32 +00:00
|
|
|
<Table.HeaderCell>Expired</Table.HeaderCell>
|
2023-06-23 00:32:26 +00:00
|
|
|
<Table.HeaderCell>Memo</Table.HeaderCell>
|
2023-06-03 18:56:01 +00:00
|
|
|
</Table.Row>
|
2023-06-23 00:32:26 +00:00
|
|
|
</Table.Header>}
|
|
|
|
|
|
|
|
<Table.Body>
|
2023-06-23 00:58:32 +00:00
|
|
|
{storageList.filter(filterStorage).sort(sortStorage).map(x =>
|
2023-06-23 00:32:26 +00:00
|
|
|
<Table.Row key={x.id}>
|
|
|
|
<Table.Cell><StorageButton storage={x} /></Table.Cell>
|
|
|
|
<Table.Cell>
|
2023-06-23 00:58:32 +00:00
|
|
|
{isMobile && 'Owner: '}
|
|
|
|
{x.member_name && <Icon name='circle' color={statusColor[x.member_status]} />}
|
|
|
|
<Link to={'/members/'+x.member_id}>{x.member_name}</Link>
|
2023-06-23 00:32:26 +00:00
|
|
|
</Table.Cell>
|
2023-06-23 00:58:32 +00:00
|
|
|
<Table.Cell>{isMobile && 'Expired: '}{x.member_paused}</Table.Cell>
|
2023-06-23 00:32:26 +00:00
|
|
|
<Table.Cell>{isMobile && 'Memo: '}{x.memo}</Table.Cell>
|
|
|
|
</Table.Row>
|
|
|
|
)}
|
|
|
|
</Table.Body>
|
|
|
|
</Table>
|
|
|
|
</>
|
2023-05-30 19:35:07 +00:00
|
|
|
:
|
|
|
|
<p>Loading...</p>
|
|
|
|
:
|
|
|
|
<p>Error loading.</p>
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
2023-05-30 23:44:05 +00:00
|
|
|
|
2023-06-03 18:56:01 +00:00
|
|
|
export function Storage(props) {
|
|
|
|
const { token, user } = props;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Container>
|
|
|
|
<Header size='large'>Storage Locations</Header>
|
|
|
|
|
|
|
|
<StorageList {...props} />
|
|
|
|
</Container>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2023-05-30 23:44:05 +00:00
|
|
|
export function ClaimShelfForm(props) {
|
|
|
|
const { token, user, refreshUser } = props;
|
|
|
|
const member = user.member;
|
|
|
|
const [input, setInput] = useState({});
|
|
|
|
const [error, setError] = useState({});
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const history = useHistory();
|
|
|
|
|
|
|
|
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);
|
|
|
|
requester('/storage/claim/', 'POST', token, input)
|
|
|
|
.then(res => {
|
|
|
|
setError({});
|
|
|
|
refreshUser();
|
|
|
|
history.push('/');
|
|
|
|
})
|
|
|
|
.catch(err => {
|
|
|
|
setLoading(false);
|
|
|
|
console.log(err);
|
|
|
|
setError(err.data);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const makeProps = (name) => ({
|
|
|
|
name: name,
|
|
|
|
onChange: handleChange,
|
|
|
|
value: input[name] || '',
|
|
|
|
error: error[name],
|
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Form onSubmit={handleSubmit}>
|
|
|
|
<div className='field'>
|
|
|
|
<label>Spaceport Username</label>
|
|
|
|
<p>{user.username}</p>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<Form.Input
|
|
|
|
label='Shelf ID'
|
|
|
|
autoComplete='off'
|
|
|
|
required
|
|
|
|
{...makeProps('shelf_id')}
|
|
|
|
maxLength={3}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<Form.Button loading={loading} error={error.non_field_errors || error.detail}>
|
|
|
|
Submit
|
|
|
|
</Form.Button>
|
|
|
|
</Form>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export function ClaimShelf(props) {
|
|
|
|
const { token, user } = props;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Container>
|
|
|
|
<Grid stackable columns={2}>
|
|
|
|
<Grid.Column>
|
|
|
|
<Header size='large'>Claim Member Shelf</Header>
|
|
|
|
|
|
|
|
<p>Use this form to claim a member shelf.</p>
|
|
|
|
|
|
|
|
<p>Please make sure your name and contact info are visible on the shelf.</p>
|
|
|
|
|
|
|
|
<p>Use the Shelf ID visible on the corner label (A1A, A2B, etc.)</p>
|
|
|
|
|
|
|
|
<ClaimShelfForm {...props} />
|
|
|
|
</Grid.Column>
|
|
|
|
</Grid>
|
|
|
|
</Container>
|
|
|
|
);
|
|
|
|
};
|