mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-02 13:24:20 +01:00
(feat) buy a new prepaid pack from the dashboard
This commit is contained in:
parent
4c4ebe3b47
commit
28b64f3c6d
@ -1,4 +1,6 @@
|
|||||||
# Changelog Fab-manager
|
# Changelog Fab-manager
|
||||||
|
- Report user's prepaid packs in the dashboard
|
||||||
|
- Ability to buy a new prepaid pack from the user's dashboard
|
||||||
|
|
||||||
- Use Time instead of DateTime objects
|
- Use Time instead of DateTime objects
|
||||||
- Fix a bug: missing statististics subtypes
|
- Fix a bug: missing statististics subtypes
|
||||||
@ -52,8 +54,7 @@
|
|||||||
- Fix a bug: unable to run task fix_invoice_item when some invoice items are associated with errors
|
- Fix a bug: unable to run task fix_invoice_item when some invoice items are associated with errors
|
||||||
- Fix a bug: invalid event date reported when the timezone in before UTC
|
- Fix a bug: invalid event date reported when the timezone in before UTC
|
||||||
- Fix a bug: unable to run accounting export if a line label was not defined
|
- Fix a bug: unable to run accounting export if a line label was not defined
|
||||||
- Fix a security issue: updated rack to 2.2.6.2 to fix [CVE-2022-44571](https
|
- Fix a security issue: updated rack to 2.2.6.2 to fix [CVE-2022-44571](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-44571)
|
||||||
- cgi-bin/cvename.cgi?name=CVE-2022-44571)
|
|
||||||
- Fix a security issue: updated globalid to 1.0.1 to fix [CVE-2023-22799](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-22799)
|
- Fix a security issue: updated globalid to 1.0.1 to fix [CVE-2023-22799](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-22799)
|
||||||
- [TODO DEPLOY] `rails fablab:fix:invoice_items_in_error` THEN `rails fablab:fix_invoice_items` THEN `rails db:migrate`
|
- [TODO DEPLOY] `rails fablab:fix:invoice_items_in_error` THEN `rails fablab:fix_invoice_items` THEN `rails db:migrate`
|
||||||
|
|
||||||
|
@ -6,28 +6,45 @@ import { UserPack } from '../../../models/user-pack';
|
|||||||
import UserPackAPI from '../../../api/user-pack';
|
import UserPackAPI from '../../../api/user-pack';
|
||||||
import FormatLib from '../../../lib/format';
|
import FormatLib from '../../../lib/format';
|
||||||
import SettingAPI from '../../../api/setting';
|
import SettingAPI from '../../../api/setting';
|
||||||
|
import { Machine } from '../../../models/machine';
|
||||||
|
import MachineAPI from '../../../api/machine';
|
||||||
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||||
|
import { FabButton } from '../../base/fab-button';
|
||||||
|
import { FormSelect } from '../../form/form-select';
|
||||||
|
import { SelectOption } from '../../../models/select';
|
||||||
|
import { ProposePacksModal } from '../../prepaid-packs/propose-packs-modal';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { User } from '../../../models/user';
|
||||||
|
|
||||||
interface PrepaidPacksPanelProps {
|
interface PrepaidPacksPanelProps {
|
||||||
userId: number,
|
user: User,
|
||||||
onError: (message: string) => void
|
onError: (message: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all available prepaid packs for the given user
|
* List all available prepaid packs for the given user
|
||||||
*/
|
*/
|
||||||
const PrepaidPacksPanel: React.FC<PrepaidPacksPanelProps> = ({ userId, onError }) => {
|
const PrepaidPacksPanel: React.FC<PrepaidPacksPanelProps> = ({ user, onError }) => {
|
||||||
const { t } = useTranslation('logged');
|
const { t } = useTranslation('logged');
|
||||||
|
|
||||||
|
const [machines, setMachines] = useState<Array<Machine>>([]);
|
||||||
const [packs, setPacks] = useState<Array<UserPack>>([]);
|
const [packs, setPacks] = useState<Array<UserPack>>([]);
|
||||||
const [threshold, setThreshold] = useState<number>(null);
|
const [threshold, setThreshold] = useState<number>(null);
|
||||||
|
const [selectedMachine, setSelectedMachine] = useState<Machine>(null);
|
||||||
|
const [packsModal, setPacksModal] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { handleSubmit, control } = useForm<{ machine_id: number }>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
UserPackAPI.index({ user_id: userId })
|
UserPackAPI.index({ user_id: user.id })
|
||||||
.then(res => setPacks(res))
|
.then(setPacks)
|
||||||
.catch(error => onError(error));
|
.catch(onError);
|
||||||
SettingAPI.get('renew_pack_threshold')
|
SettingAPI.get('renew_pack_threshold')
|
||||||
.then(data => setThreshold(parseFloat(data.value)))
|
.then(data => setThreshold(parseFloat(data.value)))
|
||||||
.catch(error => onError(error));
|
.catch(onError);
|
||||||
|
MachineAPI.index({ disabled: false })
|
||||||
|
.then(setMachines)
|
||||||
|
.catch(onError);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,6 +57,41 @@ const PrepaidPacksPanel: React.FC<PrepaidPacksPanelProps> = ({ userId, onError }
|
|||||||
return pack.prepaid_pack.minutes - pack.minutes_used <= threshold * 60;
|
return pack.prepaid_pack.minutes - pack.minutes_used <= threshold * 60;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when the user clicks on "buy a pack"
|
||||||
|
*/
|
||||||
|
const onBuyPack: SubmitHandler<{ machine_id: number }> = (data) => {
|
||||||
|
const machine = machines.find(m => m.id === data.machine_id);
|
||||||
|
setSelectedMachine(machine);
|
||||||
|
togglePacksModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open/closes the buy pack modal
|
||||||
|
*/
|
||||||
|
const togglePacksModal = () => {
|
||||||
|
setPacksModal(!packsModal);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the options for the select dropdown, for the given list of machines
|
||||||
|
*/
|
||||||
|
const buildMachinesOptions = (machines: Array<Machine>): Array<SelectOption<number>> => {
|
||||||
|
return machines.map(m => {
|
||||||
|
return { label: m.name, value: m.id };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when a prepaid pack was successfully bought: refresh the list of packs for the user
|
||||||
|
*/
|
||||||
|
const onPackBoughtSuccess = () => {
|
||||||
|
togglePacksModal();
|
||||||
|
UserPackAPI.index({ user_id: user.id })
|
||||||
|
.then(setPacks)
|
||||||
|
.catch(onError);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FabPanel className='prepaid-packs-panel'>
|
<FabPanel className='prepaid-packs-panel'>
|
||||||
<p className="title">{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.title')}</p>
|
<p className="title">{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.title')}</p>
|
||||||
@ -57,7 +109,7 @@ const PrepaidPacksPanel: React.FC<PrepaidPacksPanelProps> = ({ userId, onError }
|
|||||||
<p className="countdown"><span>{pack.minutes_used / 60}H</span> / {pack.prepaid_pack.minutes / 60}H</p>
|
<p className="countdown"><span>{pack.minutes_used / 60}H</span> / {pack.prepaid_pack.minutes / 60}H</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{ /* usage history is not saved for now
|
||||||
<div className="prepaid-packs-list is-history">
|
<div className="prepaid-packs-list is-history">
|
||||||
<span className='prepaid-packs-list-label'>{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.history')}</span>
|
<span className='prepaid-packs-list-label'>{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.history')}</span>
|
||||||
|
|
||||||
@ -66,12 +118,28 @@ const PrepaidPacksPanel: React.FC<PrepaidPacksPanelProps> = ({ userId, onError }
|
|||||||
<p className="date">00/00/00</p>
|
<p className="date">00/00/00</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
*/ }
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className='prepaid-packs-cta'>
|
<div className='prepaid-packs-cta'>
|
||||||
<p>{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.cta_info')}</p>
|
<p>{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.cta_info')}</p>
|
||||||
<button className='fab-button is-black'>{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.cta_button')}</button>
|
<form onSubmit={handleSubmit(onBuyPack)}>
|
||||||
|
<FormSelect options={buildMachinesOptions(machines)} control={control} id="machine_id" rules={{ required: true }} label={t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.select_machine')} />
|
||||||
|
<FabButton className='is-black' type="submit">
|
||||||
|
{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.cta_button')}
|
||||||
|
</FabButton>
|
||||||
|
</form>
|
||||||
|
{selectedMachine && packsModal &&
|
||||||
|
<ProposePacksModal isOpen={packsModal}
|
||||||
|
toggleModal={togglePacksModal}
|
||||||
|
item={selectedMachine}
|
||||||
|
itemType='Machine'
|
||||||
|
customer={user}
|
||||||
|
operator={user}
|
||||||
|
onError={onError}
|
||||||
|
onDecline={togglePacksModal}
|
||||||
|
onSuccess={onPackBoughtSuccess} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</FabPanel>
|
</FabPanel>
|
||||||
|
@ -8,18 +8,19 @@ import { SettingName } from '../../../models/setting';
|
|||||||
import { CreditsPanel } from './credits-panel';
|
import { CreditsPanel } from './credits-panel';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PrepaidPacksPanel } from './prepaid-packs-panel';
|
import { PrepaidPacksPanel } from './prepaid-packs-panel';
|
||||||
|
import { User } from '../../../models/user';
|
||||||
|
|
||||||
declare const Application: IApplication;
|
declare const Application: IApplication;
|
||||||
|
|
||||||
interface ReservationsDashboardProps {
|
interface ReservationsDashboardProps {
|
||||||
onError: (message: string) => void,
|
onError: (message: string) => void,
|
||||||
userId: number
|
user: User
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User dashboard showing everything about his spaces/machine reservations and also remaining credits
|
* User dashboard showing everything about his spaces/machine reservations and also remaining credits
|
||||||
*/
|
*/
|
||||||
const ReservationsDashboard: React.FC<ReservationsDashboardProps> = ({ onError, userId }) => {
|
const ReservationsDashboard: React.FC<ReservationsDashboardProps> = ({ onError, user }) => {
|
||||||
const { t } = useTranslation('logged');
|
const { t } = useTranslation('logged');
|
||||||
const [modules, setModules] = useState<Map<SettingName, string>>();
|
const [modules, setModules] = useState<Map<SettingName, string>>();
|
||||||
|
|
||||||
@ -33,17 +34,17 @@ const ReservationsDashboard: React.FC<ReservationsDashboardProps> = ({ onError,
|
|||||||
<div className="reservations-dashboard">
|
<div className="reservations-dashboard">
|
||||||
{modules?.get('machines_module') !== 'false' && <div className="section">
|
{modules?.get('machines_module') !== 'false' && <div className="section">
|
||||||
<p className="section-title">{t('app.logged.dashboard.reservations_dashboard.machine_section_title')}</p>
|
<p className="section-title">{t('app.logged.dashboard.reservations_dashboard.machine_section_title')}</p>
|
||||||
<CreditsPanel userId={userId} onError={onError} reservableType="Machine" />
|
<CreditsPanel userId={user.id} onError={onError} reservableType="Machine" />
|
||||||
<PrepaidPacksPanel userId={userId} onError={onError} />
|
<PrepaidPacksPanel user={user} onError={onError} />
|
||||||
<ReservationsPanel userId={userId} onError={onError} reservableType="Machine" />
|
<ReservationsPanel userId={user.id} onError={onError} reservableType="Machine" />
|
||||||
</div>}
|
</div>}
|
||||||
{modules?.get('spaces_module') !== 'false' && <div className="section">
|
{modules?.get('spaces_module') !== 'false' && <div className="section">
|
||||||
<p className="section-title">{t('app.logged.dashboard.reservations_dashboard.space_section_title')}</p>
|
<p className="section-title">{t('app.logged.dashboard.reservations_dashboard.space_section_title')}</p>
|
||||||
<CreditsPanel userId={userId} onError={onError} reservableType="Space" />
|
<CreditsPanel userId={user.id} onError={onError} reservableType="Space" />
|
||||||
<ReservationsPanel userId={userId} onError={onError} reservableType="Space" />
|
<ReservationsPanel userId={user.id} onError={onError} reservableType="Space" />
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Application.Components.component('reservationsDashboard', react2angular(ReservationsDashboard, ['onError', 'userId']));
|
Application.Components.component('reservationsDashboard', react2angular(ReservationsDashboard, ['onError', 'user']));
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
.prepaid-packs-panel {
|
.prepaid-packs-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: visible;
|
||||||
gap: 1.6rem;
|
gap: 1.6rem;
|
||||||
.title { @include text-base(600); }
|
.title { @include text-base(600); }
|
||||||
p { margin: 0; }
|
p { margin: 0; }
|
||||||
@ -46,11 +47,14 @@
|
|||||||
|
|
||||||
.prepaid-packs-cta {
|
.prepaid-packs-cta {
|
||||||
padding: 1.6rem;
|
padding: 1.6rem;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap:2.4rem;
|
|
||||||
background-color: var(--gray-soft-lightest);
|
background-color: var(--gray-soft-lightest);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
|
& > form {
|
||||||
|
margin-top: 1.6rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap:2.4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,5 +7,5 @@
|
|||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<reservations-dashboard user-id="user.id" on-error="onError" />
|
<reservations-dashboard user="user" on-error="onError" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -166,7 +166,8 @@ en:
|
|||||||
countdown: "Countdown"
|
countdown: "Countdown"
|
||||||
history: "History"
|
history: "History"
|
||||||
consumed_hours: "H consumed"
|
consumed_hours: "H consumed"
|
||||||
cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts"
|
cta_info: "You can buy prepaid hours packs to book machines and benefit from discounts. Choose a machine to buy a corresponding pack."
|
||||||
|
select_machine: "Select a machine"
|
||||||
cta_button: "Buy a pack"
|
cta_button: "Buy a pack"
|
||||||
#public profil of a member
|
#public profil of a member
|
||||||
members_show:
|
members_show:
|
||||||
|
@ -166,7 +166,8 @@ fr:
|
|||||||
countdown: "Décompte"
|
countdown: "Décompte"
|
||||||
history: "Historique"
|
history: "Historique"
|
||||||
consumed_hours: "H consommée(s)"
|
consumed_hours: "H consommée(s)"
|
||||||
cta_info: "Vous pouvez acheter des packs d'heures prépayées pour les machines. Ces packs vous permettent de bénéficier de remises."
|
cta_info: "Vous pouvez acheter des packs d'heures prépayées pour les machines. Ces packs vous permettent de bénéficier de remises. Choisissez une machine pour acheter un pack correspondant."
|
||||||
|
select_machine: "Selectionnez une machine"
|
||||||
cta_button: "Acheter un pack"
|
cta_button: "Acheter un pack"
|
||||||
#public profil of a member
|
#public profil of a member
|
||||||
members_show:
|
members_show:
|
||||||
|
Loading…
Reference in New Issue
Block a user