mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-29 18:52:22 +01:00
WIP: schedules management inerface
This commit is contained in:
parent
39c3164b47
commit
8d08100166
@ -2,19 +2,16 @@
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
*/
|
||||
|
||||
import React, { ReactEventHandler, useState } from 'react';
|
||||
import React, { 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 { DocumentFilters } from './document-filters';
|
||||
import moment from 'moment';
|
||||
import { IFablab } from '../models/fablab';
|
||||
import _ from 'lodash';
|
||||
import { PaymentSchedulesTable } from './payment-schedules-table';
|
||||
|
||||
declare var Application: IApplication;
|
||||
declare var Fablab: IFablab;
|
||||
|
||||
const paymentSchedulesList = PaymentScheduleAPI.list({ query: { page: 1, size: 20 } });
|
||||
|
||||
@ -22,7 +19,6 @@ const PaymentSchedulesList: React.FC = () => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [paymentSchedules, setPaymentSchedules] = useState(paymentSchedulesList.read());
|
||||
const [showExpanded, setShowExpanded] = useState({});
|
||||
|
||||
const handleFiltersChange = ({ reference, customer, date }): void => {
|
||||
const api = new PaymentScheduleAPI();
|
||||
@ -31,49 +27,6 @@ const PaymentSchedulesList: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
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">
|
||||
<h3>
|
||||
@ -83,64 +36,7 @@ const PaymentSchedulesList: React.FC = () => {
|
||||
<div className="schedules-filters">
|
||||
<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>
|
||||
|
||||
</ul>
|
||||
<PaymentSchedulesTable paymentSchedules={paymentSchedules} showCustomer={true} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,168 @@
|
||||
/**
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
*/
|
||||
|
||||
import React, { ReactEventHandler, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Loader } from './loader';
|
||||
import moment from 'moment';
|
||||
import { IFablab } from '../models/fablab';
|
||||
import _ from 'lodash';
|
||||
import { PaymentSchedule, PaymentScheduleItem, PaymentScheduleItemState } from '../models/payment-schedule';
|
||||
|
||||
declare var Fablab: IFablab;
|
||||
|
||||
interface PaymentSchedulesTableProps {
|
||||
paymentSchedules: Array<PaymentSchedule>,
|
||||
showCustomer?: boolean
|
||||
}
|
||||
|
||||
const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [showExpanded, setShowExpanded] = useState({});
|
||||
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TargetType {
|
||||
Invoice = 'invoices',
|
||||
PaymentSchedule = 'payment_schedules'
|
||||
}
|
||||
const downloadButton = (target: TargetType, id: number): JSX.Element => {
|
||||
const link = `api/${target}/${id}/download`;
|
||||
return (
|
||||
<a href={link} target="_blank" className="download-button">
|
||||
<i className="fas fa-download" />
|
||||
{t('app.admin.invoices.schedules_table.download')}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const formatState = (item: PaymentScheduleItem): JSX.Element => {
|
||||
let res = t(`app.admin.invoices.schedules_table.state_${item.state}`);
|
||||
if (item.state === PaymentScheduleItemState.Paid) {
|
||||
res += ` (${item.payment_method})`;
|
||||
}
|
||||
return <span className={`state-${item.state}`}>{res}</span>;
|
||||
}
|
||||
|
||||
const itemButtons = (item: PaymentScheduleItem): JSX.Element => {
|
||||
switch (item.state) {
|
||||
case PaymentScheduleItemState.Paid:
|
||||
return downloadButton(TargetType.Invoice, item.invoice_id);
|
||||
case PaymentScheduleItemState.Pending:
|
||||
return (<span><button>encaisser le chèque</button><button>réessayer (stripe)</button></span>);
|
||||
default:
|
||||
return <span />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="schedules-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-35" />
|
||||
<th className="w-200">{t('app.admin.invoices.schedules_table.schedule_num')}</th>
|
||||
<th className="w-200">{t('app.admin.invoices.schedules_table.date')}</th>
|
||||
<th className="w-120">{t('app.admin.invoices.schedules_table.price')}</th>
|
||||
{showCustomer && <th className="w-200">{t('app.admin.invoices.schedules_table.customer')}</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>
|
||||
{showCustomer && <td className="w-200">{p.user.name}</td>}
|
||||
<td className="w-200">{downloadButton(TargetType.PaymentSchedule, p.id)}</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">{t('app.admin.invoices.schedules_table.deadline')}</th>
|
||||
<th className="w-120">{t('app.admin.invoices.schedules_table.amount')}</th>
|
||||
<th className="w-200">{t('app.admin.invoices.schedules_table.state')}</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>{formatState(item)}</td>
|
||||
<td>{itemButtons(item)}</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
PaymentSchedulesTableComponent.defaultProps = { showCustomer: false };
|
||||
|
||||
|
||||
export const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PaymentSchedulesTableComponent paymentSchedules={paymentSchedules} showCustomer={showCustomer} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
export enum PaymentScheduleItemState {
|
||||
New = 'new',
|
||||
Pending = 'pending',
|
||||
Paid = 'paid'
|
||||
}
|
||||
export interface PaymentScheduleItem {
|
||||
id: number,
|
||||
amount: number,
|
||||
due_date: Date,
|
||||
state: string,
|
||||
state: PaymentScheduleItemState,
|
||||
invoice_id: number,
|
||||
payment_method: string,
|
||||
details: {
|
||||
|
@ -27,6 +27,7 @@
|
||||
@import "modules/stripe-modal";
|
||||
@import "modules/labelled-input";
|
||||
@import "modules/document-filters";
|
||||
@import "modules/payment-schedules-table";
|
||||
@import "modules/payment-schedules-list";
|
||||
|
||||
@import "app.responsive";
|
||||
|
@ -1,105 +1,3 @@
|
||||
.schedules-filters {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,151 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.download-button {
|
||||
color: black;
|
||||
background-color: #fbfbfb;
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
background-image: none;
|
||||
border: 1px solid #c9c9c9;
|
||||
padding: 6px 12px;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #f2f2f2;
|
||||
color: black;
|
||||
border-color: #aaaaaa;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: black;
|
||||
background-color: #f2f2f2;
|
||||
border-color: #aaaaaa;
|
||||
outline: 0;
|
||||
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
|
||||
}
|
||||
|
||||
& > i {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.state-new {
|
||||
color: #3a3a3a;
|
||||
}
|
||||
.state-pending {
|
||||
color: #d43333;
|
||||
}
|
||||
.state-paid {
|
||||
color: black;
|
||||
}
|
||||
}
|
@ -110,6 +110,7 @@ class PaymentScheduleService
|
||||
def self.list(page, size, filters = {})
|
||||
ps = PaymentSchedule.includes(:invoicing_profile, :payment_schedule_items, :subscription)
|
||||
.joins(:invoicing_profile)
|
||||
.order('payment_schedules.created_at DESC')
|
||||
.page(page)
|
||||
.per(size)
|
||||
|
||||
@ -129,7 +130,7 @@ class PaymentScheduleService
|
||||
end
|
||||
unless filters[:date].nil?
|
||||
ps = ps.where(
|
||||
"date_trunc('day', payment_schedules.created_at) = :search",
|
||||
"date_trunc('day', payment_schedules.created_at) = :search OR date_trunc('day', payment_schedule_items.due_date) = :search",
|
||||
search: "%#{DateTime.iso8601(filters[:date]).to_time.to_date}%"
|
||||
)
|
||||
end
|
||||
|
@ -1,4 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.title notification.notification_type
|
||||
json.description t('.schedule_deadline')
|
||||
json.description t('.schedule_deadline', DATE: I18n.l(notification.attached_object.due_date.to_date),
|
||||
REFERENCE: notification.attached_object.payment_schedule.reference)
|
||||
|
@ -1,4 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.title notification.notification_type
|
||||
json.description t('.schedule_failed')
|
||||
json.description t('.schedule_failed', DATE: I18n.l(notification.attached_object.due_date.to_date),
|
||||
REFERENCE: notification.attached_object.payment_schedule.reference)
|
||||
|
@ -1,4 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.title notification.notification_type
|
||||
json.description t('.schedule_failed')
|
||||
json.description t('.schedule_failed', DATE: I18n.l(notification.attached_object.due_date.to_date),
|
||||
REFERENCE: notification.attached_object.payment_schedule.reference)
|
||||
|
@ -642,6 +642,18 @@ fr:
|
||||
stripe_currency: "Devise Stripe"
|
||||
payment_schedules:
|
||||
filter_schedules: "Filtrer les échéanciers"
|
||||
schedules_table:
|
||||
schedule_num: "Échéancier n°"
|
||||
date: "Date"
|
||||
price: "Prix"
|
||||
customer: "Client"
|
||||
deadline: "Échéance"
|
||||
amount: "Montant"
|
||||
state: "État"
|
||||
download: "Télécharger"
|
||||
state_new: "Pas encore à l'échéance"
|
||||
state_pending: "Action requise"
|
||||
state_paid: "Payée"
|
||||
document_filters:
|
||||
reference: "Référence"
|
||||
customer: "Client"
|
||||
|
Loading…
x
Reference in New Issue
Block a user