1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-12-01 12:24:28 +01:00

WIP: admin interface to manage payment schedules

This commit is contained in:
Sylvain 2021-01-26 17:31:11 +01:00
parent 5e06de508d
commit 39c3164b47
9 changed files with 285 additions and 28 deletions

View File

@ -0,0 +1,57 @@
/**
* This component shows 3 input fields for filtering invoices/payment-schedules by reference, customer name and date
*/
import React, { useEffect, useState } from 'react';
import { LabelledInput } from './labelled-input';
import { useTranslation } from 'react-i18next';
interface DocumentFiltersProps {
onFilterChange: (value: { reference: string, customer: string, date: Date }) => void
}
export const DocumentFilters: React.FC<DocumentFiltersProps> = ({ onFilterChange }) => {
const { t } = useTranslation('admin');
const [referenceFilter, setReferenceFilter] = useState('');
const [customerFilter, setCustomerFilter] = useState('');
const [dateFilter, setDateFilter] = useState(null);
useEffect(() => {
onFilterChange({ reference: referenceFilter, customer: customerFilter, date: dateFilter });
}, [referenceFilter, customerFilter, dateFilter])
const handleReferenceUpdate = (e) => {
setReferenceFilter(e.target.value);
}
const handleCustomerUpdate = (e) => {
setCustomerFilter(e.target.value);
}
const handleDateUpdate = (e) => {
let date = e.target.value;
if (e.target.value === '') date = null;
setDateFilter(date);
}
return (
<div className="document-filters">
<LabelledInput id="reference"
label={t('app.admin.invoices.document_filters.reference')}
type="text"
onChange={handleReferenceUpdate}
value={referenceFilter} />
<LabelledInput id="customer"
label={t('app.admin.invoices.document_filters.customer')}
type="text"
onChange={handleCustomerUpdate}
value={customerFilter} />
<LabelledInput id="reference"
label={t('app.admin.invoices.document_filters.date')}
type="date"
onChange={handleDateUpdate}
value={dateFilter ? dateFilter : ''} />
</div>
);
}

View File

@ -2,25 +2,77 @@
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
*/
import React, { useState } from 'react';
import React, { ReactEventHandler, useState } from 'react';
import { IApplication } from '../models/application';
import { useTranslation } from 'react-i18next';
import { Loader } from './loader';
import { react2angular } from 'react2angular';
import PaymentScheduleAPI from '../api/payment-schedule';
import { LabelledInput } from './labelled-input';
import { DocumentFilters } from './document-filters';
import moment from 'moment';
import { IFablab } from '../models/fablab';
import _ from 'lodash';
declare var Application: IApplication;
declare var Fablab: IFablab;
const paymentSchedulesList = PaymentScheduleAPI.list({ query: { page: 1, size: 20 } });
const PaymentSchedulesList: React.FC = () => {
const { t } = useTranslation('admin');
const [referenceFilter, setReferenceFilter] = useState('');
const [customerFilter, setCustomerFilter] = useState('');
const [dateFilter, setDateFilter] = useState(null);
const paymentSchedules = paymentSchedulesList.read();
const [paymentSchedules, setPaymentSchedules] = useState(paymentSchedulesList.read());
const [showExpanded, setShowExpanded] = useState({});
const handleFiltersChange = ({ reference, customer, date }): void => {
const api = new PaymentScheduleAPI();
api.list({ query: { reference, customer, date, page: 1, size: 20 }}).then((res) => {
setPaymentSchedules(res);
});
};
const isExpanded = (paymentScheduleId: number): boolean => {
return showExpanded[paymentScheduleId];
}
/**
* Return the formatted localized date for the given date
*/
const formatDate = (date: Date): string => {
return Intl.DateTimeFormat().format(moment(date).toDate());
}
/**
* Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €")
*/
const formatPrice = (price: number): string => {
return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(price);
}
const statusDisplay = (paymentScheduleId: number): string => {
if (isExpanded(paymentScheduleId)) {
return 'table-row'
} else {
return 'none';
}
}
const expandCollapseIcon = (paymentScheduleId: number): JSX.Element => {
if (isExpanded(paymentScheduleId)) {
return <i className="fas fa-minus-square" />;
} else {
return <i className="fas fa-plus-square" />
}
}
const togglePaymentScheduleDetails = (paymentScheduleId: number): ReactEventHandler => {
return (): void => {
if (isExpanded(paymentScheduleId)) {
setShowExpanded(Object.assign({}, showExpanded, { [paymentScheduleId]: false }));
} else {
setShowExpanded(Object.assign({}, showExpanded, { [paymentScheduleId]: true }));
}
}
}
return (
<div className="payment-schedules-list">
@ -29,24 +81,65 @@ const PaymentSchedulesList: React.FC = () => {
{t('app.admin.invoices.payment_schedules.filter_schedules')}
</h3>
<div className="schedules-filters">
<LabelledInput id="reference"
label={t('app.admin.invoices.payment_schedules.reference')}
type="text"
onChange={setReferenceFilter}
value={referenceFilter} />
<LabelledInput id="customer"
label={t('app.admin.invoices.payment_schedules.customer')}
type="text"
onChange={setCustomerFilter}
value={customerFilter} />
<LabelledInput id="reference"
label={t('app.admin.invoices.payment_schedules.date')}
type="date"
onChange={setDateFilter}
value={dateFilter} />
<DocumentFilters onFilterChange={handleFiltersChange} />
</div>
<table className="schedules-table">
<thead>
<tr>
<th className="w-35" />
<th className="w-200">Échéancier n°</th>
<th className="w-200">Date</th>
<th className="w-120">Prix</th>
<th className="w-200">Client</th>
<th className="w-200"/>
</tr>
</thead>
<tbody>
{paymentSchedules.map(p => <tr key={p.id}>
<td colSpan={6}>
<table className="schedules-table-body">
<tbody>
<tr>
<td className="w-35 row-header" onClick={togglePaymentScheduleDetails(p.id)}>{expandCollapseIcon(p.id)}</td>
<td className="w-200">{p.reference}</td>
<td className="w-200">{formatDate(p.created_at)}</td>
<td className="w-120">{formatPrice(p.total)}</td>
<td className="w-200">{p.user.name}</td>
<td className="w-200"><button>Télécharger</button></td>
</tr>
<tr style={{ display: statusDisplay(p.id) }}>
<td className="w-35" />
<td colSpan={5}>
<div>
<table className="schedule-items-table">
<thead>
<tr>
<th className="w-120">Échéance</th>
<th className="w-120">Montant</th>
<th className="w-200">État</th>
<th className="w-200" />
</tr>
</thead>
<tbody>
{_.orderBy(p.items, 'due_date').map(item => <tr key={item.id}>
<td>{formatDate(item.due_date)}</td>
<td>{formatPrice(item.amount)}</td>
<td>{item.state} {item.state === 'paid' ? `(${item.payment_method})` : ''}</td>
<td>{item.state === 'paid' ? <button>Télécharger</button> : ''}</td>
</tr>)}
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>)}
</tbody>
</table>
<ul>
{paymentSchedules.map(p => <li>{p.reference}</li>)}
</ul>
</div>
);

View File

@ -7,7 +7,6 @@ import { Elements } from '@stripe/react-stripe-js';
import { loadStripe } from "@stripe/stripe-js";
import SettingAPI from '../api/setting';
import { SettingName } from '../models/setting';
import { Loader } from './loader';
const stripePublicKey = SettingAPI.get(SettingName.StripePublicKey);

View File

@ -26,6 +26,7 @@
@import "modules/wallet-info";
@import "modules/stripe-modal";
@import "modules/labelled-input";
@import "modules/document-filters";
@import "modules/payment-schedules-list";
@import "app.responsive";

View File

@ -0,0 +1,8 @@
.document-filters {
display: flex;
justify-content: space-between;
& > * {
width: 31%;
}
}

View File

@ -1,8 +1,105 @@
.schedules-filters {
display: flex;
justify-content: space-between;
margin-bottom: 2em;
}
& > * {
width: 31%;
.schedules-table {
table-layout: fixed;
border: 1px solid #e9e9e9;
border-top: 0;
margin-bottom: 0;
width: 100%;
max-width: 100%;
background-color: transparent;
border-collapse: collapse;
border-spacing: 0;
& > thead {
border-top: 1px solid #e9e9e9;
& > tr > th {
font-weight: 600;
vertical-align: middle;
text-align: center;
padding: 2rem 1rem;
line-height: 1.5;
border: 1px solid #f0f0f0;
border-top: 0;
}
}
.w-35 { width: 35px; }
.w-120 { width: 120px; }
.w-200 { width: 200px; }
.schedules-table-body {
table-layout: fixed;
background-color: #fff;
border: 1px solid #e9e9e9;
border-top: 0;
margin-bottom: 0;
width: 100%;
max-width: 100%;
border-collapse: collapse;
border-spacing: 0;
& > tbody {
background: #f7f7f9;
border-collapse: collapse;
border-spacing: 0;
line-height: 1.5;
& > tr > td {
padding: 12px 10px;
border: 1px solid #f0f0f0;
border-top: 0;
vertical-align: middle;
font-size: 1.4rem;
line-height: 1.5;
&.row-header {
text-align: center;
cursor: pointer;
}
}
.schedule-items-table {
table-layout: fixed;
background-color: #fff;
width: 100%;
max-width: 100%;
margin-bottom: 1rem;
border-collapse: collapse;
border-spacing: 0;
border: 1px solid #e9e9e9;
border-top: 0;
& > thead {
border-top: 1px solid #e9e9e9;
font-size: 1.4rem;
line-height: 1.5;
& > tr > th {
border: 1px solid #f0f0f0;
border-top: 0;
border-bottom: 1px solid #e9e9e9;
font-weight: 600;
vertical-align: middle;
text-align: center;
font-size: 1.1rem;
padding: 2rem 1rem;
text-transform: uppercase;
}
}
& > tbody > tr > td {
vertical-align: middle;
border: 1px solid #f0f0f0;
border-top: 0;
padding: 12px 10px;
font-size: 1.4rem;
line-height: 1.5;
}
}
}
}
}

View File

@ -11,6 +11,6 @@ class PaymentScheduleItem < Footprintable
end
def self.columns_out_of_footprint
%w[invoice_id stp_invoice_id]
%w[invoice_id stp_invoice_id state payment_method]
end
end

View File

@ -642,6 +642,7 @@ en:
stripe_currency: "Stripe currency"
payment_schedules:
filter_schedules: "Filter schedules"
document_filters:
reference: "Reference"
customer: "Customer"
date: "Date"

View File

@ -642,6 +642,7 @@ fr:
stripe_currency: "Devise Stripe"
payment_schedules:
filter_schedules: "Filtrer les échéanciers"
document_filters:
reference: "Référence"
customer: "Client"
date: "Date"