chore: add husky pre-commit hook and lint-staged configuration
- Created a pre-commit hook in .husky to run lint-staged. - Added .lintstagedrc.mjs to format frontend files with Prettier and apply Spotless to backend Java files. - Introduced .prettierignore to exclude specific directories from formatting.
This commit is contained in:
1 parent
5d2d85aa8c
commit
878da155bd
564 files changed
+61467
-47179
No files matched your search
@@ -4,14 +4,14 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import {
|
||||
EligibleRentDue,
|
||||
RentPaymentAllocationDialog,
|
||||
EligibleRentDue,
|
||||
RentPaymentAllocationDialog,
|
||||
} from '../rent-payment-allocation-dialog/rent-payment-allocation-dialog';
|
||||
import { RentPaymentRefundDialog } from '../rent-payment-refund-dialog/rent-payment-refund-dialog';
|
||||
import { RentPaymentReversalDialog } from '../rent-payment-reversal-dialog/rent-payment-reversal-dialog';
|
||||
import {
|
||||
RentPaymentValidationDialog,
|
||||
RentPaymentValidationDialogData,
|
||||
RentPaymentValidationDialog,
|
||||
RentPaymentValidationDialogData,
|
||||
} from '../rent-payment-validation-dialog/rent-payment-validation-dialog';
|
||||
import { RentDue } from '../../services/lease.service';
|
||||
import { formatEuro } from '../../services/rent-payment-format';
|
||||
@@ -22,224 +22,230 @@ import { RentDueCorrection } from '../rent-due-correction/rent-due-correction';
|
||||
import { formatFrenchCivilDate } from '../../services/civil-date';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rent-payments-page',
|
||||
imports: [MatButtonModule, MatProgressBarModule, MatProgressSpinnerModule, RentDueCorrection],
|
||||
templateUrl: './rent-payments-page.html',
|
||||
styleUrl: './rent-payments-page.css',
|
||||
selector: 'app-rent-payments-page',
|
||||
imports: [MatButtonModule, MatProgressBarModule, MatProgressSpinnerModule, RentDueCorrection],
|
||||
templateUrl: './rent-payments-page.html',
|
||||
styleUrl: './rent-payments-page.css',
|
||||
})
|
||||
export class RentPaymentsPage {
|
||||
readonly workspace = inject(WorkspaceService);
|
||||
private readonly dialog = inject(MatDialog);
|
||||
readonly showFullSchedule = signal(false);
|
||||
readonly feedback = signal<{ kind: 'success' | 'error'; message: string } | null>(null);
|
||||
readonly formatEuro = formatEuro;
|
||||
readonly civilDate = formatFrenchCivilDate;
|
||||
readonly displayedRentDues = computed(() =>
|
||||
this.showFullSchedule()
|
||||
? sortRentDues(this.workspace.rentDues())
|
||||
: currentRentDues(this.workspace.rentDues()),
|
||||
);
|
||||
readonly displayedPayments = computed(() =>
|
||||
[...this.workspace.rentPayments()].sort(
|
||||
(left, right) =>
|
||||
right.receivedAt.localeCompare(left.receivedAt) ||
|
||||
right.confirmedAt.localeCompare(left.confirmedAt),
|
||||
),
|
||||
);
|
||||
readonly scheduleReady = computed(
|
||||
() =>
|
||||
this.workspace.rentDueLoadState().loaded &&
|
||||
this.workspace.leaseLoadState().loaded &&
|
||||
this.workspace.apartmentLoadState().loaded,
|
||||
);
|
||||
readonly scheduleLoading = computed(
|
||||
() =>
|
||||
this.workspace.rentDueLoadState().loading ||
|
||||
this.workspace.leaseLoadState().loading ||
|
||||
this.workspace.apartmentLoadState().loading,
|
||||
);
|
||||
readonly scheduleError = computed(
|
||||
() =>
|
||||
this.workspace.rentDueLoadState().error ||
|
||||
this.workspace.leaseLoadState().error ||
|
||||
this.workspace.apartmentLoadState().error,
|
||||
);
|
||||
readonly workspace = inject(WorkspaceService);
|
||||
private readonly dialog = inject(MatDialog);
|
||||
readonly showFullSchedule = signal(false);
|
||||
readonly feedback = signal<{ kind: 'success' | 'error'; message: string } | null>(null);
|
||||
readonly formatEuro = formatEuro;
|
||||
readonly civilDate = formatFrenchCivilDate;
|
||||
readonly displayedRentDues = computed(() =>
|
||||
this.showFullSchedule()
|
||||
? sortRentDues(this.workspace.rentDues())
|
||||
: currentRentDues(this.workspace.rentDues()),
|
||||
);
|
||||
readonly displayedPayments = computed(() =>
|
||||
[...this.workspace.rentPayments()].sort(
|
||||
(left, right) =>
|
||||
right.receivedAt.localeCompare(left.receivedAt) ||
|
||||
right.confirmedAt.localeCompare(left.confirmedAt),
|
||||
),
|
||||
);
|
||||
readonly scheduleReady = computed(
|
||||
() =>
|
||||
this.workspace.rentDueLoadState().loaded &&
|
||||
this.workspace.leaseLoadState().loaded &&
|
||||
this.workspace.apartmentLoadState().loaded,
|
||||
);
|
||||
readonly scheduleLoading = computed(
|
||||
() =>
|
||||
this.workspace.rentDueLoadState().loading ||
|
||||
this.workspace.leaseLoadState().loading ||
|
||||
this.workspace.apartmentLoadState().loading,
|
||||
);
|
||||
readonly scheduleError = computed(
|
||||
() =>
|
||||
this.workspace.rentDueLoadState().error ||
|
||||
this.workspace.leaseLoadState().error ||
|
||||
this.workspace.apartmentLoadState().error,
|
||||
);
|
||||
|
||||
constructor() {
|
||||
this.load();
|
||||
}
|
||||
constructor() {
|
||||
this.load();
|
||||
}
|
||||
|
||||
apartmentName(rentDue: RentDue): string {
|
||||
const lease = this.workspace.leases().find((item) => item.id === rentDue.leaseId);
|
||||
return (
|
||||
this.workspace.apartments().find((item) => item.id === lease?.apartmentId)?.name ??
|
||||
'Bien inconnu'
|
||||
);
|
||||
}
|
||||
apartmentName(rentDue: RentDue): string {
|
||||
const lease = this.workspace.leases().find((item) => item.id === rentDue.leaseId);
|
||||
return (
|
||||
this.workspace.apartments().find((item) => item.id === lease?.apartmentId)?.name ??
|
||||
'Bien inconnu'
|
||||
);
|
||||
}
|
||||
|
||||
monthLabel(month: string): string {
|
||||
return new Intl.DateTimeFormat('fr-FR', { month: 'long', year: 'numeric' }).format(
|
||||
new Date(`${month}-01T12:00:00`),
|
||||
);
|
||||
}
|
||||
monthLabel(month: string): string {
|
||||
return new Intl.DateTimeFormat('fr-FR', { month: 'long', year: 'numeric' }).format(
|
||||
new Date(`${month}-01T12:00:00`),
|
||||
);
|
||||
}
|
||||
|
||||
settlementLabel(settlement: RentDue['settlement']): string {
|
||||
return { UNPAID: 'Non réglée', PARTIALLY_PAID: 'Partiellement réglée', PAID: 'Réglée' }[
|
||||
settlement
|
||||
];
|
||||
}
|
||||
settlementLabel(settlement: RentDue['settlement']): string {
|
||||
return { UNPAID: 'Non réglée', PARTIALLY_PAID: 'Partiellement réglée', PAID: 'Réglée' }[
|
||||
settlement
|
||||
];
|
||||
}
|
||||
|
||||
timingLabel(timing: RentDue['timing']): string {
|
||||
return { UPCOMING: 'À venir', OVERDUE: 'En retard' }[timing];
|
||||
}
|
||||
timingLabel(timing: RentDue['timing']): string {
|
||||
return { UPCOMING: 'À venir', OVERDUE: 'En retard' }[timing];
|
||||
}
|
||||
|
||||
paymentStatusLabel(status: RentPayment['status']): string {
|
||||
return { CONFIRMED: 'Confirmé', REVERSED: 'Contrepassé' }[status];
|
||||
}
|
||||
paymentStatusLabel(status: RentPayment['status']): string {
|
||||
return { CONFIRMED: 'Confirmé', REVERSED: 'Contrepassé' }[status];
|
||||
}
|
||||
|
||||
allocationLabel(allocation: PaymentAllocation): string {
|
||||
const due = this.workspace.rentDues().find((item) => item.id === allocation.rentDueId);
|
||||
return due ? `${this.apartmentName(due)} — ${this.monthLabel(due.month)}` : 'Échéance inconnue';
|
||||
}
|
||||
allocationLabel(allocation: PaymentAllocation): string {
|
||||
const due = this.workspace.rentDues().find((item) => item.id === allocation.rentDueId);
|
||||
return due
|
||||
? `${this.apartmentName(due)} — ${this.monthLabel(due.month)}`
|
||||
: 'Échéance inconnue';
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.workspace.loadRentDues();
|
||||
this.workspace.loadLeases();
|
||||
this.workspace.loadApartments();
|
||||
this.workspace.loadRentPayments();
|
||||
}
|
||||
load(): void {
|
||||
this.workspace.loadRentDues();
|
||||
this.workspace.loadLeases();
|
||||
this.workspace.loadApartments();
|
||||
this.workspace.loadRentPayments();
|
||||
}
|
||||
|
||||
toggleSchedule(): void {
|
||||
this.showFullSchedule.update((showFullSchedule) => !showFullSchedule);
|
||||
}
|
||||
toggleSchedule(): void {
|
||||
this.showFullSchedule.update((showFullSchedule) => !showFullSchedule);
|
||||
}
|
||||
|
||||
recordPayment(rentDue: RentDue): void {
|
||||
this.dialog
|
||||
.open<RentPaymentValidationDialog, RentPaymentValidationDialogData, RentPaymentDialogResult>(
|
||||
RentPaymentValidationDialog,
|
||||
{
|
||||
data: { rentDue, apartmentName: this.apartmentName(rentDue) },
|
||||
width: 'min(36rem, calc(100vw - 2rem))',
|
||||
maxWidth: 'calc(100vw - 2rem)',
|
||||
},
|
||||
)
|
||||
.afterClosed()
|
||||
.subscribe((payment) => this.accept(payment, 'Le règlement a été enregistré.'));
|
||||
}
|
||||
recordPayment(rentDue: RentDue): void {
|
||||
this.dialog
|
||||
.open<
|
||||
RentPaymentValidationDialog,
|
||||
RentPaymentValidationDialogData,
|
||||
RentPaymentDialogResult
|
||||
>(RentPaymentValidationDialog, {
|
||||
data: { rentDue, apartmentName: this.apartmentName(rentDue) },
|
||||
width: 'min(36rem, calc(100vw - 2rem))',
|
||||
maxWidth: 'calc(100vw - 2rem)',
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((payment) => this.accept(payment, 'Le règlement a été enregistré.'));
|
||||
}
|
||||
|
||||
allocateSurplus(payment: RentPayment): void {
|
||||
const eligibleRentDues = this.eligibleRentDues(payment);
|
||||
if (eligibleRentDues.length === 0) return;
|
||||
this.dialog
|
||||
.open<
|
||||
RentPaymentAllocationDialog,
|
||||
{ payment: RentPayment; eligibleRentDues: EligibleRentDue[] },
|
||||
RentPaymentDialogResult
|
||||
>(RentPaymentAllocationDialog, {
|
||||
data: { payment, eligibleRentDues },
|
||||
width: 'min(36rem, calc(100vw - 2rem))',
|
||||
maxWidth: 'calc(100vw - 2rem)',
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((updated) => this.accept(updated, 'Le surplus a été affecté.'));
|
||||
}
|
||||
allocateSurplus(payment: RentPayment): void {
|
||||
const eligibleRentDues = this.eligibleRentDues(payment);
|
||||
if (eligibleRentDues.length === 0) return;
|
||||
this.dialog
|
||||
.open<
|
||||
RentPaymentAllocationDialog,
|
||||
{ payment: RentPayment; eligibleRentDues: EligibleRentDue[] },
|
||||
RentPaymentDialogResult
|
||||
>(RentPaymentAllocationDialog, {
|
||||
data: { payment, eligibleRentDues },
|
||||
width: 'min(36rem, calc(100vw - 2rem))',
|
||||
maxWidth: 'calc(100vw - 2rem)',
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((updated) => this.accept(updated, 'Le surplus a été affecté.'));
|
||||
}
|
||||
|
||||
refundSurplus(payment: RentPayment): void {
|
||||
this.dialog
|
||||
.open<RentPaymentRefundDialog, RentPayment, RentPaymentDialogResult>(
|
||||
RentPaymentRefundDialog,
|
||||
{
|
||||
data: payment,
|
||||
width: 'min(34rem, calc(100vw - 2rem))',
|
||||
maxWidth: 'calc(100vw - 2rem)',
|
||||
},
|
||||
)
|
||||
.afterClosed()
|
||||
.subscribe((updated) => this.accept(updated, 'Le remboursement a été enregistré.'));
|
||||
}
|
||||
refundSurplus(payment: RentPayment): void {
|
||||
this.dialog
|
||||
.open<RentPaymentRefundDialog, RentPayment, RentPaymentDialogResult>(
|
||||
RentPaymentRefundDialog,
|
||||
{
|
||||
data: payment,
|
||||
width: 'min(34rem, calc(100vw - 2rem))',
|
||||
maxWidth: 'calc(100vw - 2rem)',
|
||||
},
|
||||
)
|
||||
.afterClosed()
|
||||
.subscribe((updated) => this.accept(updated, 'Le remboursement a été enregistré.'));
|
||||
}
|
||||
|
||||
reversePayment(payment: RentPayment): void {
|
||||
this.dialog
|
||||
.open<RentPaymentReversalDialog, RentPayment, RentPaymentDialogResult>(
|
||||
RentPaymentReversalDialog,
|
||||
{
|
||||
data: payment,
|
||||
role: 'alertdialog',
|
||||
width: 'min(34rem, calc(100vw - 2rem))',
|
||||
maxWidth: 'calc(100vw - 2rem)',
|
||||
},
|
||||
)
|
||||
.afterClosed()
|
||||
.subscribe((updated) => this.accept(updated, 'Le règlement a été contrepassé.'));
|
||||
}
|
||||
reversePayment(payment: RentPayment): void {
|
||||
this.dialog
|
||||
.open<RentPaymentReversalDialog, RentPayment, RentPaymentDialogResult>(
|
||||
RentPaymentReversalDialog,
|
||||
{
|
||||
data: payment,
|
||||
role: 'alertdialog',
|
||||
width: 'min(34rem, calc(100vw - 2rem))',
|
||||
maxWidth: 'calc(100vw - 2rem)',
|
||||
},
|
||||
)
|
||||
.afterClosed()
|
||||
.subscribe((updated) => this.accept(updated, 'Le règlement a été contrepassé.'));
|
||||
}
|
||||
|
||||
canAllocate(payment: RentPayment): boolean {
|
||||
return payment.unallocatedBalance > 0 && this.eligibleRentDues(payment).length > 0;
|
||||
}
|
||||
canAllocate(payment: RentPayment): boolean {
|
||||
return payment.unallocatedBalance > 0 && this.eligibleRentDues(payment).length > 0;
|
||||
}
|
||||
|
||||
private eligibleRentDues(payment: RentPayment): EligibleRentDue[] {
|
||||
const allocatedDueIds = new Set(payment.allocations.map((allocation) => allocation.rentDueId));
|
||||
return sortRentDues(this.workspace.rentDues())
|
||||
.filter(
|
||||
(due) =>
|
||||
due.leaseId === payment.leaseId &&
|
||||
due.settlement !== 'PAID' &&
|
||||
!allocatedDueIds.has(due.id),
|
||||
)
|
||||
.map((rentDue) => ({
|
||||
rentDue,
|
||||
label: `${this.apartmentName(rentDue)} — ${this.monthLabel(rentDue.month)}`,
|
||||
}));
|
||||
}
|
||||
private eligibleRentDues(payment: RentPayment): EligibleRentDue[] {
|
||||
const allocatedDueIds = new Set(
|
||||
payment.allocations.map((allocation) => allocation.rentDueId),
|
||||
);
|
||||
return sortRentDues(this.workspace.rentDues())
|
||||
.filter(
|
||||
(due) =>
|
||||
due.leaseId === payment.leaseId &&
|
||||
due.settlement !== 'PAID' &&
|
||||
!allocatedDueIds.has(due.id),
|
||||
)
|
||||
.map((rentDue) => ({
|
||||
rentDue,
|
||||
label: `${this.apartmentName(rentDue)} — ${this.monthLabel(rentDue.month)}`,
|
||||
}));
|
||||
}
|
||||
|
||||
private accept(result: RentPaymentDialogResult | undefined, message: string): void {
|
||||
if (!result) return;
|
||||
if (result === RENT_PAYMENT_DATA_STALE) {
|
||||
this.workspace.loadRentDues();
|
||||
this.workspace.loadRentPayments();
|
||||
this.feedback.set({
|
||||
kind: 'error',
|
||||
message: 'Les données ont changé. Elles ont été actualisées ; recommencez l’opération.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.workspace.acceptRentPayment(result);
|
||||
this.feedback.set({ kind: 'success', message });
|
||||
}
|
||||
private accept(result: RentPaymentDialogResult | undefined, message: string): void {
|
||||
if (!result) return;
|
||||
if (result === RENT_PAYMENT_DATA_STALE) {
|
||||
this.workspace.loadRentDues();
|
||||
this.workspace.loadRentPayments();
|
||||
this.feedback.set({
|
||||
kind: 'error',
|
||||
message:
|
||||
'Les données ont changé. Elles ont été actualisées ; recommencez l’opération.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.workspace.acceptRentPayment(result);
|
||||
this.feedback.set({ kind: 'success', message });
|
||||
}
|
||||
}
|
||||
|
||||
function sortRentDues(rentDues: readonly RentDue[]): RentDue[] {
|
||||
return [...rentDues].sort((left, right) => left.dueDate.localeCompare(right.dueDate));
|
||||
return [...rentDues].sort((left, right) => left.dueDate.localeCompare(right.dueDate));
|
||||
}
|
||||
|
||||
function currentRentDues(rentDues: readonly RentDue[]): RentDue[] {
|
||||
const rentDuesByLease = new Map<string, RentDue[]>();
|
||||
for (const rentDue of sortRentDues(rentDues)) {
|
||||
const schedule = rentDuesByLease.get(rentDue.leaseId);
|
||||
if (schedule) schedule.push(rentDue);
|
||||
else rentDuesByLease.set(rentDue.leaseId, [rentDue]);
|
||||
}
|
||||
return sortRentDues([...rentDuesByLease.values()].flatMap(currentSchedule));
|
||||
const rentDuesByLease = new Map<string, RentDue[]>();
|
||||
for (const rentDue of sortRentDues(rentDues)) {
|
||||
const schedule = rentDuesByLease.get(rentDue.leaseId);
|
||||
if (schedule) schedule.push(rentDue);
|
||||
else rentDuesByLease.set(rentDue.leaseId, [rentDue]);
|
||||
}
|
||||
return sortRentDues([...rentDuesByLease.values()].flatMap(currentSchedule));
|
||||
}
|
||||
|
||||
function currentSchedule(schedule: RentDue[]): RentDue[] {
|
||||
const nextDueIndex = schedule.findIndex((rentDue) => rentDue.timing === 'UPCOMING');
|
||||
const lastCorrectionIndex = schedule.reduce(
|
||||
(lastCorrection, rentDue, index) => (rentDue.correctionRequired ? index : lastCorrection),
|
||||
-1,
|
||||
);
|
||||
const visibleEndIndex =
|
||||
nextDueIndex === -1 ? schedule.length - 1 : Math.max(nextDueIndex, lastCorrectionIndex);
|
||||
const throughNextDue = schedule.slice(0, visibleEndIndex + 1);
|
||||
const lastPaidIndex = throughNextDue.reduce(
|
||||
(lastPaid, rentDue, index) => (rentDue.settlement === 'PAID' ? index : lastPaid),
|
||||
-1,
|
||||
);
|
||||
const firstUnpaidIndex = throughNextDue.findIndex((rentDue) => rentDue.settlement !== 'PAID');
|
||||
const firstCorrectionIndex = throughNextDue.findIndex((rentDue) => rentDue.correctionRequired);
|
||||
const firstVisibleIndex = Math.min(
|
||||
...[lastPaidIndex, firstUnpaidIndex, firstCorrectionIndex].filter((index) => index !== -1),
|
||||
);
|
||||
const nextDueIndex = schedule.findIndex((rentDue) => rentDue.timing === 'UPCOMING');
|
||||
const lastCorrectionIndex = schedule.reduce(
|
||||
(lastCorrection, rentDue, index) => (rentDue.correctionRequired ? index : lastCorrection),
|
||||
-1,
|
||||
);
|
||||
const visibleEndIndex =
|
||||
nextDueIndex === -1 ? schedule.length - 1 : Math.max(nextDueIndex, lastCorrectionIndex);
|
||||
const throughNextDue = schedule.slice(0, visibleEndIndex + 1);
|
||||
const lastPaidIndex = throughNextDue.reduce(
|
||||
(lastPaid, rentDue, index) => (rentDue.settlement === 'PAID' ? index : lastPaid),
|
||||
-1,
|
||||
);
|
||||
const firstUnpaidIndex = throughNextDue.findIndex((rentDue) => rentDue.settlement !== 'PAID');
|
||||
const firstCorrectionIndex = throughNextDue.findIndex((rentDue) => rentDue.correctionRequired);
|
||||
const firstVisibleIndex = Math.min(
|
||||
...[lastPaidIndex, firstUnpaidIndex, firstCorrectionIndex].filter((index) => index !== -1),
|
||||
);
|
||||
|
||||
return firstVisibleIndex === Infinity ? [] : throughNextDue.slice(firstVisibleIndex);
|
||||
return firstVisibleIndex === Infinity ? [] : throughNextDue.slice(firstVisibleIndex);
|
||||
}
|
||||
Reference in new issue
Block a user