Merge pull request #195 from goteo/feat/charges-search-filters-tags
[feat] Charges Search filters tags + restructure Contributions.svelte (other sub task has been discovered)
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Slider from "./Slider.svelte";
|
||||
import Filters from "./Filters.svelte";
|
||||
import Table from "./Table.svelte";
|
||||
import Categories from "./Categories.svelte";
|
||||
import FiltersTags from "./FiltersTags.svelte";
|
||||
import { t } from "../../i18n/store";
|
||||
import ExportCsv from "./ExportCsv.svelte";
|
||||
import type { ApiGatewayChargesGetCollectionData } from "../../openapi/client";
|
||||
|
||||
/* TODO: Add data to slider and set locales */
|
||||
const slides = [
|
||||
@@ -12,20 +17,28 @@
|
||||
{ title: "Slide 5", amount: "250,99€" },
|
||||
];
|
||||
|
||||
let filters = $state({
|
||||
paymentMethod: "",
|
||||
chargeStatus: "",
|
||||
rangeAmount: "",
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
target: undefined,
|
||||
});
|
||||
let filters: ApiGatewayChargesGetCollectionData["query"] = $state({});
|
||||
|
||||
function handleApplyFilters(newFilters) {
|
||||
function handleApplyFilters(newFilters: ApiGatewayChargesGetCollectionData["query"]) {
|
||||
filters = { ...filters, ...newFilters };
|
||||
}
|
||||
</script>
|
||||
|
||||
<Filters onApplyFilters={handleApplyFilters} currentTarget={filters.target} />
|
||||
<Slider {slides} />
|
||||
<div>
|
||||
<Filters {filters} onApplyFilters={handleApplyFilters} currentTarget={filters?.target} />
|
||||
<div>
|
||||
<div>
|
||||
<FiltersTags
|
||||
onCloseFilter={handleApplyFilters}
|
||||
title={$t("admin.charges.lastContributions")}
|
||||
{filters}
|
||||
/>
|
||||
<!-- TODO: Move "Export .csv" button to this component (from Filters.svelte) -->
|
||||
<ExportCsv />
|
||||
</div>
|
||||
<!-- TODO: Implement Categories component -->
|
||||
<Categories />
|
||||
<Slider {slides} />
|
||||
</div>
|
||||
</div>
|
||||
<Table {filters} />
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
import ActiveFilterIcon from "../../svgs/ActiveFilterIcon.svelte";
|
||||
import Search from "./Search.svelte";
|
||||
import { apiGatewaysGetCollection } from "../../openapi/client/index";
|
||||
import { apiGatewayChargesGetCollection } from "../../openapi/client/index";
|
||||
import {
|
||||
apiGatewayChargesGetCollection,
|
||||
type ApiGatewayChargesGetCollectionData,
|
||||
} from "../../openapi/client/index";
|
||||
import { t } from "../../i18n/store";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let { onApplyFilters, currentTarget } = $props<{
|
||||
let { filters, onApplyFilters, currentTarget } = $props<{
|
||||
filters: ApiGatewayChargesGetCollectionData["query"];
|
||||
onApplyFilters: (filters: any) => void;
|
||||
currentTarget?: string;
|
||||
}>();
|
||||
@@ -54,15 +58,17 @@
|
||||
alert($t("contributions.filters.dateRange.errors.invalidRange"));
|
||||
return;
|
||||
}
|
||||
|
||||
onApplyFilters({
|
||||
paymentMethod: selectedPaymentMethod,
|
||||
chargeStatus: selectedChargeStatus,
|
||||
rangeAmount: selectedRangeAmount,
|
||||
from: dateFrom
|
||||
? new Date(new Date(dateFrom).getTime() - 24 * 60 * 60 * 1000).toISOString()
|
||||
...filters,
|
||||
"checkout.gateway": selectedPaymentMethod || undefined,
|
||||
status: selectedChargeStatus || undefined,
|
||||
"money.amount[gte]": selectedRangeAmount || undefined,
|
||||
"dateCreated[after]": dateFrom
|
||||
? new Date(new Date(dateFrom).getTime()).toISOString()
|
||||
: undefined,
|
||||
to: dateTo
|
||||
? new Date(new Date(dateTo).getTime() + 24 * 60 * 60 * 1000).toISOString()
|
||||
"dateCreated[before]": dateTo
|
||||
? new Date(new Date(dateTo).getTime()).toISOString()
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
@@ -94,17 +100,13 @@
|
||||
}
|
||||
|
||||
if (dateFrom) {
|
||||
const fromDate = new Date(
|
||||
new Date(dateFrom).getTime() - 24 * 60 * 60 * 1000,
|
||||
).toISOString();
|
||||
const fromDate = new Date(new Date(dateFrom).getTime()).toISOString();
|
||||
queryParams["dateCreated[strictly_after]"] = fromDate;
|
||||
filenameParams.from = dateFrom;
|
||||
}
|
||||
|
||||
if (dateTo) {
|
||||
const toDate = new Date(
|
||||
new Date(dateTo).getTime() + 24 * 60 * 60 * 1000,
|
||||
).toISOString();
|
||||
const toDate = new Date(new Date(dateTo).getTime()).toISOString();
|
||||
queryParams["dateCreated[strictly_before]"] = toDate;
|
||||
filenameParams.to = dateTo;
|
||||
}
|
||||
@@ -127,8 +129,8 @@
|
||||
while (hasMoreData) {
|
||||
const pageQueryParams = {
|
||||
...queryParams,
|
||||
itemsPerPage: "50",
|
||||
page: currentPage.toString(),
|
||||
itemsPerPage: 50,
|
||||
page: currentPage,
|
||||
};
|
||||
|
||||
const pageResponse = await apiGatewayChargesGetCollection({
|
||||
@@ -208,6 +210,16 @@
|
||||
isExporting = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (typeof filters["checkout.gateway"] === "undefined") selectedPaymentMethod = "";
|
||||
if (typeof filters.status === "undefined") selectedChargeStatus = "";
|
||||
if (typeof filters["money.amount[gte]"] === "undefined") selectedRangeAmount = "";
|
||||
if (typeof filters["money.amount[between]"] === "undefined") selectedRangeAmount = "";
|
||||
|
||||
if (typeof filters["dateCreated[after]"] === "undefined") dateFrom = "";
|
||||
if (typeof filters["dateCreated[before]"] === "undefined") dateTo = "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -224,7 +236,7 @@
|
||||
>
|
||||
<span class="relative">
|
||||
<FiltersIcon />
|
||||
{#if selectedPaymentMethod || selectedChargeStatus || selectedRangeAmount || dateFrom || dateTo}
|
||||
{#if selectedPaymentMethod !== "" || selectedChargeStatus !== "" || selectedRangeAmount !== "" || dateFrom !== "" || dateTo !== ""}
|
||||
<span class="absolute -top-1 -right-1">
|
||||
<ActiveFilterIcon />
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
import CloseIcon from "../../svgs/CloseIcon.svelte";
|
||||
import Tag from "../library/Tag.svelte";
|
||||
import { locale } from "../../i18n/store";
|
||||
import type { Locale } from "../../i18n/locales";
|
||||
import { t } from "../../i18n/store";
|
||||
import type { ApiGatewayChargesGetCollectionData } from "../../openapi/client";
|
||||
import { formatDate } from "../../utils/dates";
|
||||
|
||||
let { title, filters, onCloseFilter } = $props<{
|
||||
title: string;
|
||||
filters: ApiGatewayChargesGetCollectionData["query"];
|
||||
onCloseFilter: (filters: ApiGatewayChargesGetCollectionData["query"]) => void;
|
||||
}>();
|
||||
|
||||
type FilterTag = { title: string; value?: string; values?: { from?: string; to?: string } };
|
||||
type FilterTags = FilterTag[];
|
||||
|
||||
let tags: FilterTags | undefined = $state(undefined);
|
||||
|
||||
function closeTag(tag: FilterTag) {
|
||||
if (tag.values?.from && tag.values?.to) {
|
||||
onCloseFilter({
|
||||
...filters,
|
||||
["dateCreated[after]"]: undefined,
|
||||
["dateCreated[before]"]: undefined,
|
||||
});
|
||||
} else {
|
||||
onCloseFilter({
|
||||
...filters,
|
||||
[tag.title]: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function formatTags(tags: FilterTags, locale: Locale) {
|
||||
if (tags === undefined) return;
|
||||
|
||||
tags.map((tag) => {
|
||||
if (tag.values?.from && tag.values?.to) {
|
||||
tag.values.from = formatDate(new Date(tag.values.from), locale);
|
||||
tag.values.to = formatDate(new Date(tag.values.to), locale);
|
||||
}
|
||||
|
||||
if (tag.title === "checkout.gateway") {
|
||||
tag.value = $t(`contributions.filters.paymentMethod.options.${tag.value}`);
|
||||
}
|
||||
|
||||
if (tag.title === "status") {
|
||||
tag.value = $t(`contributions.filters.chargeStatus.options.${tag.value}`);
|
||||
}
|
||||
|
||||
if (tag.title === "money.amount[between]") {
|
||||
}
|
||||
|
||||
if (tag.title === "money.amount[gte]")
|
||||
tag.value = $t(`contributions.filters.rangeAmount.options.${tag.value}`);
|
||||
});
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (filters !== undefined) {
|
||||
let dateFrom = "dateCreated[after]";
|
||||
let dateTo = "dateCreated[before]";
|
||||
|
||||
let normalTags: FilterTags | undefined = Object.keys(filters)
|
||||
.map((filter) => {
|
||||
if (filter === dateFrom || filter === dateTo) return { title: filter };
|
||||
else return { title: filter, value: filters[filter] };
|
||||
})
|
||||
.filter((filter) => {
|
||||
if (filter.title === dateFrom || filter.title === dateTo) return false;
|
||||
if (filter.value === undefined) return filter.value !== undefined;
|
||||
if (filter.value === "all") return false;
|
||||
else return filter.value !== "";
|
||||
});
|
||||
|
||||
if (
|
||||
filters[dateFrom] !== "" &&
|
||||
filters[dateTo] !== "" &&
|
||||
typeof filters[dateFrom] !== "undefined" &&
|
||||
typeof filters[dateTo] !== "undefined"
|
||||
) {
|
||||
normalTags = [
|
||||
...normalTags,
|
||||
{
|
||||
title: "date",
|
||||
values: {
|
||||
from: filters[dateFrom],
|
||||
to: filters[dateTo],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
tags = formatTags([...normalTags], $locale);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<h1 class="text-2xl/8 font-bold text-black">
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
{#each tags as tag}
|
||||
<Tag variant={"bold"}>
|
||||
{#if tag.values}
|
||||
{`${tag.values.from} - ${tag.values.to}`}
|
||||
{:else}
|
||||
{tag.value}
|
||||
{/if}
|
||||
<button onclick={() => closeTag(tag)} class="size-auto cursor-pointer">
|
||||
<CloseIcon class="size-[15px]" />
|
||||
</button>
|
||||
</Tag>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -29,6 +29,7 @@
|
||||
Accounting,
|
||||
ApiProjectsIdOrSlugGetData,
|
||||
ApiUsersIdGetData,
|
||||
ApiGatewayChargesGetCollectionData,
|
||||
} from "../../../src/openapi/client/index.ts";
|
||||
import DetailsRow from "./DetailsRow.svelte";
|
||||
import { client } from "../../openapi/client/client.gen.ts";
|
||||
@@ -207,11 +208,11 @@
|
||||
}
|
||||
|
||||
async function loadCharges(filters: {
|
||||
chargeStatus: string;
|
||||
rangeAmount: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
paymentMethod: string;
|
||||
chargeStatus: string;
|
||||
rangeAmount?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
target?: string;
|
||||
}) {
|
||||
const current = Number(itemsPerPage);
|
||||
@@ -272,8 +273,8 @@
|
||||
(filters.rangeAmount.includes("..")
|
||||
? { "money.amount[between]": filters.rangeAmount }
|
||||
: { "money.amount[gte]": filters.rangeAmount })),
|
||||
...(filters.from && { "dateCreated[strictly_after]": filters.from }),
|
||||
...(filters.to && { "dateCreated[strictly_before]": filters.to }),
|
||||
...(filters.dateFrom && { "dateCreated[strictly_after]": filters.dateFrom }),
|
||||
...(filters.dateTo && { "dateCreated[strictly_before]": filters.dateTo }),
|
||||
...(filters.paymentMethod &&
|
||||
filters.paymentMethod !== "all" && {
|
||||
"checkout.gateway": `/v4/gateways/${filters.paymentMethod}`,
|
||||
@@ -487,21 +488,12 @@
|
||||
};
|
||||
}
|
||||
|
||||
let { filters } = $props<{
|
||||
filters: {
|
||||
paymentMethod: string;
|
||||
chargeStatus: string;
|
||||
rangeAmount: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
target?: string;
|
||||
};
|
||||
}>();
|
||||
let { filters } = $props<ApiGatewayChargesGetCollectionData["query"]>();
|
||||
|
||||
$effect(() => {
|
||||
const { chargeStatus, rangeAmount, from, to, paymentMethod, target } = filters;
|
||||
const { paymentMethod, chargeStatus, rangeAmount, dateFrom, dateTo, target } = filters;
|
||||
charges = [];
|
||||
loadCharges({ chargeStatus, rangeAmount, from, to, paymentMethod, target });
|
||||
loadCharges({ paymentMethod, chargeStatus, rangeAmount, dateFrom, dateTo, target });
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
success: "bg-variant2",
|
||||
warning: "bg-variant4",
|
||||
error: "bg-tertiary",
|
||||
bold: "bg-grey border-grey text-secondary font-bold",
|
||||
bold: "bg-grey border-grey text-secondary font-bold border-soft-purple",
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={twMerge(
|
||||
"flex content-center items-center gap-2 rounded-[4px] border border-black bg-white px-2 py-1 whitespace-nowrap text-black",
|
||||
"flex items-center justify-center gap-2 rounded-sm border border-black bg-white px-2 py-1 text-[14px] whitespace-nowrap text-black",
|
||||
variant ? variantStyles[variant] : "",
|
||||
classes,
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import { onMount } from "svelte";
|
||||
import type { User } from "../../openapi/client/types.gen.ts";
|
||||
import { extractId } from "../../utils/extractId.ts";
|
||||
import { locale } from "../../i18n/store";
|
||||
import { formatDate } from "../../utils/dates.ts";
|
||||
|
||||
interface Props {
|
||||
update: ProjectUpdate;
|
||||
@@ -22,16 +24,6 @@
|
||||
let cardClasses = $state("");
|
||||
let author: User | undefined = $state(undefined);
|
||||
|
||||
function formatDate(date: string, locale?: string): string {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
};
|
||||
|
||||
return new Date(date).toLocaleDateString(locale, options);
|
||||
}
|
||||
|
||||
async function getAuthor(update: ProjectUpdate): Promise<User | undefined> {
|
||||
const authorId: string | null = extractId(update.author);
|
||||
if (!authorId) return undefined;
|
||||
@@ -61,13 +53,15 @@
|
||||
{#if type === "small"}
|
||||
<div
|
||||
class={twMerge(
|
||||
"flex w-[34.625rem] flex-col gap-6 rounded-4xl bg-white p-6 opacity-48",
|
||||
"flex w-138.5 flex-col gap-6 rounded-4xl bg-white p-6 opacity-48",
|
||||
cardClasses,
|
||||
)}
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-secondary flex flex-row gap-0.5 text-2xl font-bold">
|
||||
{formatDate(update.date ?? "")}
|
||||
{#if update.date}
|
||||
{formatDate(new Date(update.date), $locale)}
|
||||
{/if}
|
||||
<div class="pt-1">
|
||||
<ActiveFilterIcon />
|
||||
</div>
|
||||
@@ -80,7 +74,7 @@
|
||||
{update.title}
|
||||
</h2>
|
||||
<svg
|
||||
class="absolute -top-[19.25rem] left-[6.1875rem]"
|
||||
class="absolute -top-77 left-24.75"
|
||||
width="681"
|
||||
height="964.575"
|
||||
viewBox="0 0 407 533"
|
||||
@@ -104,7 +98,9 @@
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-secondary flex flex-row gap-0.5 text-2xl font-bold">
|
||||
{formatDate(update.date ?? "")}
|
||||
{#if update.date}
|
||||
{formatDate(new Date(update.date), $locale)}
|
||||
{/if}
|
||||
<div class="pt-1">
|
||||
<ActiveFilterIcon />
|
||||
</div>
|
||||
|
||||
@@ -605,8 +605,7 @@
|
||||
"wallet": "Moneder"
|
||||
},
|
||||
"status": {
|
||||
"in_pending": "Pendent",
|
||||
"charged": "Cobrat",
|
||||
"in_charge": "Cobrat",
|
||||
"to_refund": "Per reemborsar",
|
||||
"refunded": "Reemborsat"
|
||||
},
|
||||
@@ -717,8 +716,7 @@
|
||||
"title": "Estat de l'aportació",
|
||||
"options": {
|
||||
"all": "Tots",
|
||||
"in_pending": "Pendent",
|
||||
"charged": "Cobrat",
|
||||
"in_charge": "Cobrat",
|
||||
"to_refund": "Per reemborsar",
|
||||
"refunded": "Reemborsat"
|
||||
}
|
||||
|
||||
@@ -597,8 +597,7 @@
|
||||
"wallet": "Wallet"
|
||||
},
|
||||
"status": {
|
||||
"in_pending": "Pending",
|
||||
"charged": "Charged",
|
||||
"in_charge": "Charged",
|
||||
"to_refund": "To refund",
|
||||
"refunded": "Refunded"
|
||||
},
|
||||
@@ -709,8 +708,6 @@
|
||||
"title": "Charge status",
|
||||
"options": {
|
||||
"all": "All",
|
||||
"in_pending": "Pending",
|
||||
"charged": "Charged",
|
||||
"to_refund": "To be refunded",
|
||||
"refunded": "Refunded"
|
||||
}
|
||||
|
||||
@@ -601,10 +601,12 @@
|
||||
"wallet": "Monedero"
|
||||
},
|
||||
"status": {
|
||||
"in_pending": "Pendiente",
|
||||
"charged": "Cobrado",
|
||||
"to_charge": "Por cobrar",
|
||||
"in_charge": "Cobrado",
|
||||
"to_refund": "Por reembolsar",
|
||||
"refunded": "Reembolsado"
|
||||
"refunded": "Reembolsado",
|
||||
"to_wallet": "Por reembolsar a monedero",
|
||||
"walleted": "Reembolsado a monedero"
|
||||
},
|
||||
"refund": {
|
||||
"to_wallet": "Sí",
|
||||
@@ -713,10 +715,12 @@
|
||||
"title": "Estado de Aporte",
|
||||
"options": {
|
||||
"all": "Todos",
|
||||
"in_pending": "Pendiente",
|
||||
"charged": "Cobrado",
|
||||
"to_charge": "Por cobrar",
|
||||
"in_charge": "Cobrado",
|
||||
"to_refund": "Por reembolsar",
|
||||
"refunded": "Reembolsado"
|
||||
"refunded": "Reembolsado",
|
||||
"to_wallet": "Por reembolsar a monedero",
|
||||
"walleted": "Reembolsado a monedero"
|
||||
}
|
||||
},
|
||||
"projectStatus": {
|
||||
@@ -881,6 +885,9 @@
|
||||
"totalPaid": "Total pagado:",
|
||||
"totalUnpaid": "Falta por pagar:"
|
||||
}
|
||||
},
|
||||
"charges": {
|
||||
"lastContributions": "Últimos aportes:"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
<script lang="ts">
|
||||
import type { ClassNameValue } from "tailwind-merge";
|
||||
|
||||
interface Props {
|
||||
class?: ClassNameValue;
|
||||
}
|
||||
|
||||
let { class: classes }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class={classes ? classes : ""}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"> </g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
|
||||
|
Before Width: | Height: | Size: 594 B After Width: | Height: | Size: 837 B |
@@ -0,0 +1,11 @@
|
||||
import type { Locale } from "../i18n/locales";
|
||||
|
||||
export function formatDate(date: Date, locale: Locale) {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
};
|
||||
|
||||
return date.toLocaleDateString(locale, options);
|
||||
}
|
||||
Reference in New Issue
Block a user