ads-nuxt 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +577 -546
- package/app/app.config.ts +25 -25
- package/app/assets/ads/checkbox-check.svg +5 -5
- package/app/assets/ads/checkbox-minus.svg +5 -5
- package/app/assets/brand/farmarcas.svg +6 -6
- package/app/assets/brand/radar.svg +9 -9
- package/app/assets/css/component-tokens.css +403 -403
- package/app/assets/css/main.css +617 -617
- package/app/components/AdsBadge.vue +48 -48
- package/app/components/AdsBroadcast.vue +45 -45
- package/app/components/AdsButton.vue +569 -569
- package/app/components/AdsButtonGroup.vue +39 -39
- package/app/components/AdsButtonMenu.vue +48 -48
- package/app/components/AdsCard.vue +117 -117
- package/app/components/AdsCharttip.vue +78 -78
- package/app/components/AdsCheckbox.vue +77 -77
- package/app/components/AdsDatePicker.vue +473 -473
- package/app/components/AdsDropdownMenu.vue +19 -19
- package/app/components/AdsEmptyState.vue +58 -58
- package/app/components/AdsFarmarcasPill.vue +74 -74
- package/app/components/AdsFeatureCard.vue +217 -217
- package/app/components/AdsFileCard.vue +101 -101
- package/app/components/AdsFilterDropdown.vue +174 -174
- package/app/components/AdsFilterOption.vue +72 -72
- package/app/components/AdsFilterSelect.vue +202 -202
- package/app/components/AdsFooter.vue +25 -25
- package/app/components/AdsInput.vue +143 -143
- package/app/components/AdsListDropdown.vue +44 -44
- package/app/components/AdsLoading.vue +109 -109
- package/app/components/AdsMenuBar.vue +103 -103
- package/app/components/AdsMetricCard.vue +119 -119
- package/app/components/AdsMiniPill.vue +40 -40
- package/app/components/AdsModal.vue +157 -157
- package/app/components/AdsMonthPicker.vue +80 -80
- package/app/components/AdsNavbarApp.vue +67 -67
- package/app/components/AdsNavbarDesk.vue +62 -62
- package/app/components/AdsNotabilityPill.vue +27 -27
- package/app/components/AdsNumberInput.vue +166 -166
- package/app/components/AdsPagination.vue +91 -91
- package/app/components/AdsProductTag.vue +61 -61
- package/app/components/AdsRadio.vue +70 -70
- package/app/components/AdsSearchInput.vue +113 -113
- package/app/components/AdsSegmentedTabs.vue +28 -28
- package/app/components/AdsSideMenu.vue +101 -101
- package/app/components/AdsSmalltip.vue +44 -44
- package/app/components/AdsSteptip.vue +101 -101
- package/app/components/AdsSwitch.vue +59 -59
- package/app/components/AdsTable.vue +100 -100
- package/app/components/AdsTableToolbar.vue +44 -44
- package/app/components/AdsTabs.vue +51 -51
- package/app/components/AdsTextarea.vue +109 -109
- package/app/components/AdsToast.vue +181 -181
- package/app/components/AdsTooltip.vue +66 -66
- package/app/components/InvertedTooltip.vue +40 -40
- package/app/components/OverlayLoading.vue +173 -173
- package/app/components/ProgressBarLoading.vue +256 -256
- package/app/composables/useOverlayLoading.ts +66 -66
- package/app/locale/pt-br.ts +6 -6
- package/app/plugins/ads-toast.client.ts +24 -24
- package/app/theme/ads-theme.ts +370 -370
- package/app/theme/component-tokens.json +402 -402
- package/app/theme/component-tokens.ts +815 -815
- package/app/types/overlay-loading.types.ts +26 -26
- package/app/utils/ads-filter-select.ts +172 -172
- package/app/utils/ads-table.ts +204 -204
- package/app/utils/calendar-date.ts +128 -128
- package/app/utils/category-role.ts +45 -45
- package/app/utils/money.ts +77 -77
- package/app/utils/notability.ts +8 -8
- package/app/utils/parser.ts +134 -134
- package/app/utils/percentage.ts +54 -54
- package/nuxt.config.ts +31 -32
- package/package.json +55 -46
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
export const CATEGORY_ROLES = [
|
|
2
|
-
"destination",
|
|
3
|
-
"routine",
|
|
4
|
-
"seasonal",
|
|
5
|
-
"convenience",
|
|
6
|
-
] as const;
|
|
7
|
-
|
|
8
|
-
export type CategoryRole = (typeof CATEGORY_ROLES)[number];
|
|
9
|
-
|
|
10
|
-
const CATEGORY_ROLE_LABELS: Record<CategoryRole, string> = {
|
|
11
|
-
destination: "Destino",
|
|
12
|
-
routine: "Rotina",
|
|
13
|
-
seasonal: "Sazonal",
|
|
14
|
-
convenience: "Conveniência",
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const CATEGORY_ROLE_OPTIONS = CATEGORY_ROLES.map((role) => ({
|
|
18
|
-
label: CATEGORY_ROLE_LABELS[role],
|
|
19
|
-
value: role,
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
export const formatCategoryRole = (role: CategoryRole | null): string =>
|
|
23
|
-
role ? CATEGORY_ROLE_LABELS[role] : "-";
|
|
24
|
-
|
|
25
|
-
const stripDiacritics = (value: string): string =>
|
|
26
|
-
value.normalize("NFD").replace(/\p{Diacritic}/gu, "");
|
|
27
|
-
|
|
28
|
-
export const parseCategoryRole = (
|
|
29
|
-
raw: string | null | undefined,
|
|
30
|
-
): CategoryRole | null => {
|
|
31
|
-
const normalized = stripDiacritics(String(raw ?? "").trim()).toLowerCase();
|
|
32
|
-
|
|
33
|
-
if (!normalized) return null;
|
|
34
|
-
|
|
35
|
-
if (normalized.startsWith("destin")) return "destination";
|
|
36
|
-
if (normalized.startsWith("rotin") || normalized.startsWith("routin")) {
|
|
37
|
-
return "routine";
|
|
38
|
-
}
|
|
39
|
-
if (normalized.startsWith("sazon") || normalized.startsWith("season")) {
|
|
40
|
-
return "seasonal";
|
|
41
|
-
}
|
|
42
|
-
if (normalized.startsWith("conven")) return "convenience";
|
|
43
|
-
|
|
44
|
-
return null;
|
|
45
|
-
};
|
|
1
|
+
export const CATEGORY_ROLES = [
|
|
2
|
+
"destination",
|
|
3
|
+
"routine",
|
|
4
|
+
"seasonal",
|
|
5
|
+
"convenience",
|
|
6
|
+
] as const;
|
|
7
|
+
|
|
8
|
+
export type CategoryRole = (typeof CATEGORY_ROLES)[number];
|
|
9
|
+
|
|
10
|
+
const CATEGORY_ROLE_LABELS: Record<CategoryRole, string> = {
|
|
11
|
+
destination: "Destino",
|
|
12
|
+
routine: "Rotina",
|
|
13
|
+
seasonal: "Sazonal",
|
|
14
|
+
convenience: "Conveniência",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const CATEGORY_ROLE_OPTIONS = CATEGORY_ROLES.map((role) => ({
|
|
18
|
+
label: CATEGORY_ROLE_LABELS[role],
|
|
19
|
+
value: role,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
export const formatCategoryRole = (role: CategoryRole | null): string =>
|
|
23
|
+
role ? CATEGORY_ROLE_LABELS[role] : "-";
|
|
24
|
+
|
|
25
|
+
const stripDiacritics = (value: string): string =>
|
|
26
|
+
value.normalize("NFD").replace(/\p{Diacritic}/gu, "");
|
|
27
|
+
|
|
28
|
+
export const parseCategoryRole = (
|
|
29
|
+
raw: string | null | undefined,
|
|
30
|
+
): CategoryRole | null => {
|
|
31
|
+
const normalized = stripDiacritics(String(raw ?? "").trim()).toLowerCase();
|
|
32
|
+
|
|
33
|
+
if (!normalized) return null;
|
|
34
|
+
|
|
35
|
+
if (normalized.startsWith("destin")) return "destination";
|
|
36
|
+
if (normalized.startsWith("rotin") || normalized.startsWith("routin")) {
|
|
37
|
+
return "routine";
|
|
38
|
+
}
|
|
39
|
+
if (normalized.startsWith("sazon") || normalized.startsWith("season")) {
|
|
40
|
+
return "seasonal";
|
|
41
|
+
}
|
|
42
|
+
if (normalized.startsWith("conven")) return "convenience";
|
|
43
|
+
|
|
44
|
+
return null;
|
|
45
|
+
};
|
package/app/utils/money.ts
CHANGED
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
export const numberToBRL = (
|
|
2
|
-
price: number | undefined | null,
|
|
3
|
-
withSignal = false
|
|
4
|
-
): string => {
|
|
5
|
-
if (price == null) return "-";
|
|
6
|
-
|
|
7
|
-
const sign = price < 0 ? "-" : withSignal && price > 0 ? "+" : "";
|
|
8
|
-
const value = new Intl.NumberFormat("pt-BR", {
|
|
9
|
-
style: "currency",
|
|
10
|
-
currency: "BRL",
|
|
11
|
-
}).format(Math.abs(price));
|
|
12
|
-
|
|
13
|
-
return `R$ ${sign}${value.replace("R$", "").trim()}`;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const MILLION = 1_000_000;
|
|
17
|
-
const THOUSAND = 1_000;
|
|
18
|
-
|
|
19
|
-
const COMPACT_DECIMAL_FORMATTER = new Intl.NumberFormat("pt-BR", {
|
|
20
|
-
minimumFractionDigits: 2,
|
|
21
|
-
maximumFractionDigits: 2,
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
export const numberToCompactBRL = (
|
|
25
|
-
value: number | undefined | null,
|
|
26
|
-
): string => {
|
|
27
|
-
if (value == null || Number.isNaN(value)) return "-";
|
|
28
|
-
|
|
29
|
-
const sign = value < 0 ? "-" : "";
|
|
30
|
-
const absolute = Math.abs(value);
|
|
31
|
-
|
|
32
|
-
if (absolute >= MILLION) {
|
|
33
|
-
return `R$ ${sign}${COMPACT_DECIMAL_FORMATTER.format(absolute / MILLION)}MM`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (absolute >= THOUSAND) {
|
|
37
|
-
return `R$ ${sign}${COMPACT_DECIMAL_FORMATTER.format(absolute / THOUSAND)}M`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return numberToBRL(value);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export const parseStringMoneyToFloat = (value: string): number | null => {
|
|
44
|
-
if (!value || typeof value !== "string") return null;
|
|
45
|
-
const cleanValue = value.replace(/[^\d,.-]/g, "");
|
|
46
|
-
if (!cleanValue) return null;
|
|
47
|
-
const normalizedValue = cleanValue.replace(",", ".");
|
|
48
|
-
const parsed = parseFloat(normalizedValue);
|
|
49
|
-
return Number.isNaN(parsed) ? null : parsed;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const BRL_INPUT_FORMATTER = new Intl.NumberFormat("pt-BR", {
|
|
53
|
-
minimumFractionDigits: 2,
|
|
54
|
-
maximumFractionDigits: 2,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Formata valor numérico para exibição em input monetário (padrão R$).
|
|
59
|
-
* pt-BR: vírgula decimal, ponto milhares, 2 decimais. Sem símbolo R$.
|
|
60
|
-
* Use o valor retornado apenas para exibição; persista sempre o número.
|
|
61
|
-
*/
|
|
62
|
-
export function formatPriceInput(value?: number | null): string {
|
|
63
|
-
if (value === undefined || value === null || Number.isNaN(value)) return "";
|
|
64
|
-
return BRL_INPUT_FORMATTER.format(Number(value.toFixed(2)));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Converte o valor digitado em máscara monetária para número em reais.
|
|
69
|
-
* Sanitiza (apenas dígitos), interpreta como centavos (últimos 2 = decimais)
|
|
70
|
-
* e retorna valor para persistência no backend.
|
|
71
|
-
*/
|
|
72
|
-
export function parsePriceInput(value: string | number): number {
|
|
73
|
-
if (typeof value === "number" && !Number.isNaN(value))
|
|
74
|
-
return Math.round(value * 100) / 100;
|
|
75
|
-
const digits = String(value ?? "").replace(/\D/g, "");
|
|
76
|
-
return parseInt(digits || "0", 10) / 100;
|
|
77
|
-
}
|
|
1
|
+
export const numberToBRL = (
|
|
2
|
+
price: number | undefined | null,
|
|
3
|
+
withSignal = false
|
|
4
|
+
): string => {
|
|
5
|
+
if (price == null) return "-";
|
|
6
|
+
|
|
7
|
+
const sign = price < 0 ? "-" : withSignal && price > 0 ? "+" : "";
|
|
8
|
+
const value = new Intl.NumberFormat("pt-BR", {
|
|
9
|
+
style: "currency",
|
|
10
|
+
currency: "BRL",
|
|
11
|
+
}).format(Math.abs(price));
|
|
12
|
+
|
|
13
|
+
return `R$ ${sign}${value.replace("R$", "").trim()}`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const MILLION = 1_000_000;
|
|
17
|
+
const THOUSAND = 1_000;
|
|
18
|
+
|
|
19
|
+
const COMPACT_DECIMAL_FORMATTER = new Intl.NumberFormat("pt-BR", {
|
|
20
|
+
minimumFractionDigits: 2,
|
|
21
|
+
maximumFractionDigits: 2,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const numberToCompactBRL = (
|
|
25
|
+
value: number | undefined | null,
|
|
26
|
+
): string => {
|
|
27
|
+
if (value == null || Number.isNaN(value)) return "-";
|
|
28
|
+
|
|
29
|
+
const sign = value < 0 ? "-" : "";
|
|
30
|
+
const absolute = Math.abs(value);
|
|
31
|
+
|
|
32
|
+
if (absolute >= MILLION) {
|
|
33
|
+
return `R$ ${sign}${COMPACT_DECIMAL_FORMATTER.format(absolute / MILLION)}MM`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (absolute >= THOUSAND) {
|
|
37
|
+
return `R$ ${sign}${COMPACT_DECIMAL_FORMATTER.format(absolute / THOUSAND)}M`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return numberToBRL(value);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const parseStringMoneyToFloat = (value: string): number | null => {
|
|
44
|
+
if (!value || typeof value !== "string") return null;
|
|
45
|
+
const cleanValue = value.replace(/[^\d,.-]/g, "");
|
|
46
|
+
if (!cleanValue) return null;
|
|
47
|
+
const normalizedValue = cleanValue.replace(",", ".");
|
|
48
|
+
const parsed = parseFloat(normalizedValue);
|
|
49
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const BRL_INPUT_FORMATTER = new Intl.NumberFormat("pt-BR", {
|
|
53
|
+
minimumFractionDigits: 2,
|
|
54
|
+
maximumFractionDigits: 2,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Formata valor numérico para exibição em input monetário (padrão R$).
|
|
59
|
+
* pt-BR: vírgula decimal, ponto milhares, 2 decimais. Sem símbolo R$.
|
|
60
|
+
* Use o valor retornado apenas para exibição; persista sempre o número.
|
|
61
|
+
*/
|
|
62
|
+
export function formatPriceInput(value?: number | null): string {
|
|
63
|
+
if (value === undefined || value === null || Number.isNaN(value)) return "";
|
|
64
|
+
return BRL_INPUT_FORMATTER.format(Number(value.toFixed(2)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Converte o valor digitado em máscara monetária para número em reais.
|
|
69
|
+
* Sanitiza (apenas dígitos), interpreta como centavos (últimos 2 = decimais)
|
|
70
|
+
* e retorna valor para persistência no backend.
|
|
71
|
+
*/
|
|
72
|
+
export function parsePriceInput(value: string | number): number {
|
|
73
|
+
if (typeof value === "number" && !Number.isNaN(value))
|
|
74
|
+
return Math.round(value * 100) / 100;
|
|
75
|
+
const digits = String(value ?? "").replace(/\D/g, "");
|
|
76
|
+
return parseInt(digits || "0", 10) / 100;
|
|
77
|
+
}
|
package/app/utils/notability.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const NOTABILITY_LABELS: Record<string, string> = {
|
|
2
|
-
traffic: "Gerador de Tráfego",
|
|
3
|
-
image: "Formador de Imagem",
|
|
4
|
-
profit: "Gerador de Lucro",
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export const formatNotability = (notability: string): string =>
|
|
8
|
-
NOTABILITY_LABELS[notability] ?? notability;
|
|
1
|
+
const NOTABILITY_LABELS: Record<string, string> = {
|
|
2
|
+
traffic: "Gerador de Tráfego",
|
|
3
|
+
image: "Formador de Imagem",
|
|
4
|
+
profit: "Gerador de Lucro",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const formatNotability = (notability: string): string =>
|
|
8
|
+
NOTABILITY_LABELS[notability] ?? notability;
|
package/app/utils/parser.ts
CHANGED
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
export const parseToPercentage = (value: number) =>
|
|
2
|
-
Number((value * 100).toFixed(2));
|
|
3
|
-
|
|
4
|
-
export const parsePercentageToFloat = (value: string | number) => {
|
|
5
|
-
if (typeof value === "string") {
|
|
6
|
-
value = value.replace(",", ".");
|
|
7
|
-
return Number(value) / 100;
|
|
8
|
-
}
|
|
9
|
-
return value / 100;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const parseFloatToDecimal = (value: number) => {
|
|
13
|
-
return value.toLocaleString("pt-BR", {
|
|
14
|
-
minimumFractionDigits: 2,
|
|
15
|
-
maximumFractionDigits: 2,
|
|
16
|
-
});
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
20
|
-
|
|
21
|
-
const BUSINESS_CALENDAR_TIMEZONE = "America/Sao_Paulo";
|
|
22
|
-
|
|
23
|
-
const getCalendarPart = (
|
|
24
|
-
parts: Intl.DateTimeFormatPart[],
|
|
25
|
-
type: Intl.DateTimeFormatPartTypes,
|
|
26
|
-
): string | undefined => parts.find((part) => part.type === type)?.value;
|
|
27
|
-
|
|
28
|
-
export function getTodayDateString(now: Date = new Date()): string {
|
|
29
|
-
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
30
|
-
timeZone: BUSINESS_CALENDAR_TIMEZONE,
|
|
31
|
-
year: "numeric",
|
|
32
|
-
month: "2-digit",
|
|
33
|
-
day: "2-digit",
|
|
34
|
-
}).formatToParts(now);
|
|
35
|
-
|
|
36
|
-
const year = getCalendarPart(parts, "year");
|
|
37
|
-
const month = getCalendarPart(parts, "month");
|
|
38
|
-
const day = getCalendarPart(parts, "day");
|
|
39
|
-
|
|
40
|
-
if (!year || !month || !day) {
|
|
41
|
-
throw new Error(
|
|
42
|
-
"Não foi possível obter a data civil em America/Sao_Paulo",
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return `${year}-${month}-${day}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function toDateOnlyString(dateStr: string): string {
|
|
50
|
-
return dateStr.includes("T") ? dateStr.slice(0, 10) : dateStr;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export const extractDateOnly = (value: string | Date): string => {
|
|
54
|
-
if (typeof value === "string") {
|
|
55
|
-
if (DATE_ONLY_REGEX.test(value)) return value;
|
|
56
|
-
const iso = value.trim();
|
|
57
|
-
if (iso.length >= 10) return iso.slice(0, 10);
|
|
58
|
-
return "";
|
|
59
|
-
}
|
|
60
|
-
const iso = value.toISOString();
|
|
61
|
-
return iso.slice(0, 10);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
export const formatDateOnlyToPtBr = (value: string | Date): string => {
|
|
66
|
-
const yyyyMmDd = extractDateOnly(value);
|
|
67
|
-
if (!yyyyMmDd || !DATE_ONLY_REGEX.test(yyyyMmDd)) return "";
|
|
68
|
-
|
|
69
|
-
const [y, m, d] = yyyyMmDd.split("-").map(Number);
|
|
70
|
-
const day = String(d).padStart(2, "0");
|
|
71
|
-
const month = String(m).padStart(2, "0");
|
|
72
|
-
const year = String(y);
|
|
73
|
-
|
|
74
|
-
return `${day}/${month}/${year}`;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const daysBetweenDateOnly = (start: string, end: string): number => {
|
|
78
|
-
const startStr = extractDateOnly(start);
|
|
79
|
-
const endStr = extractDateOnly(end);
|
|
80
|
-
if (!startStr || !endStr) return 0;
|
|
81
|
-
|
|
82
|
-
const startDate = new Date(startStr + "T12:00:00");
|
|
83
|
-
const endDate = new Date(endStr + "T12:00:00");
|
|
84
|
-
const diffMs = endDate.getTime() - startDate.getTime();
|
|
85
|
-
return Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export const daysFromTodayToDateOnly = (isoOrDateStr: string): number => {
|
|
89
|
-
const endStr = extractDateOnly(isoOrDateStr);
|
|
90
|
-
if (!endStr) return 0;
|
|
91
|
-
|
|
92
|
-
const todayStr = getTodayDateString();
|
|
93
|
-
const todayNoon = new Date(todayStr + "T12:00:00");
|
|
94
|
-
const endNoon = new Date(endStr + "T12:00:00");
|
|
95
|
-
const diffMs = endNoon.getTime() - todayNoon.getTime();
|
|
96
|
-
return Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export const formatDateToPtBr = (date: Date | string, onlyDate = false): string => {
|
|
100
|
-
const d = new Date(date);
|
|
101
|
-
if (isNaN(d.getTime())) return "";
|
|
102
|
-
|
|
103
|
-
if (onlyDate) {
|
|
104
|
-
return d.toLocaleDateString("pt-BR");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return d.toLocaleDateString("pt-BR") + " " + d.toLocaleTimeString("pt-BR", {
|
|
108
|
-
hour: "2-digit",
|
|
109
|
-
minute: "2-digit"
|
|
110
|
-
});
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
export const normalizePercentage = (
|
|
114
|
-
value: string | number | undefined,
|
|
115
|
-
fallback?: number
|
|
116
|
-
): number => {
|
|
117
|
-
if (value === undefined || value === null || value === "") {
|
|
118
|
-
return fallback ?? 0;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (typeof value === "string") {
|
|
122
|
-
const numericValue = Number(value.replace(",", "."));
|
|
123
|
-
if (Number.isNaN(numericValue)) {
|
|
124
|
-
return fallback ?? 0;
|
|
125
|
-
}
|
|
126
|
-
return parsePercentageToFloat(numericValue);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (typeof value === "number") {
|
|
130
|
-
return parsePercentageToFloat(value);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return fallback ?? 0;
|
|
134
|
-
};
|
|
1
|
+
export const parseToPercentage = (value: number) =>
|
|
2
|
+
Number((value * 100).toFixed(2));
|
|
3
|
+
|
|
4
|
+
export const parsePercentageToFloat = (value: string | number) => {
|
|
5
|
+
if (typeof value === "string") {
|
|
6
|
+
value = value.replace(",", ".");
|
|
7
|
+
return Number(value) / 100;
|
|
8
|
+
}
|
|
9
|
+
return value / 100;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const parseFloatToDecimal = (value: number) => {
|
|
13
|
+
return value.toLocaleString("pt-BR", {
|
|
14
|
+
minimumFractionDigits: 2,
|
|
15
|
+
maximumFractionDigits: 2,
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
20
|
+
|
|
21
|
+
const BUSINESS_CALENDAR_TIMEZONE = "America/Sao_Paulo";
|
|
22
|
+
|
|
23
|
+
const getCalendarPart = (
|
|
24
|
+
parts: Intl.DateTimeFormatPart[],
|
|
25
|
+
type: Intl.DateTimeFormatPartTypes,
|
|
26
|
+
): string | undefined => parts.find((part) => part.type === type)?.value;
|
|
27
|
+
|
|
28
|
+
export function getTodayDateString(now: Date = new Date()): string {
|
|
29
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
30
|
+
timeZone: BUSINESS_CALENDAR_TIMEZONE,
|
|
31
|
+
year: "numeric",
|
|
32
|
+
month: "2-digit",
|
|
33
|
+
day: "2-digit",
|
|
34
|
+
}).formatToParts(now);
|
|
35
|
+
|
|
36
|
+
const year = getCalendarPart(parts, "year");
|
|
37
|
+
const month = getCalendarPart(parts, "month");
|
|
38
|
+
const day = getCalendarPart(parts, "day");
|
|
39
|
+
|
|
40
|
+
if (!year || !month || !day) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"Não foi possível obter a data civil em America/Sao_Paulo",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `${year}-${month}-${day}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function toDateOnlyString(dateStr: string): string {
|
|
50
|
+
return dateStr.includes("T") ? dateStr.slice(0, 10) : dateStr;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const extractDateOnly = (value: string | Date): string => {
|
|
54
|
+
if (typeof value === "string") {
|
|
55
|
+
if (DATE_ONLY_REGEX.test(value)) return value;
|
|
56
|
+
const iso = value.trim();
|
|
57
|
+
if (iso.length >= 10) return iso.slice(0, 10);
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
const iso = value.toISOString();
|
|
61
|
+
return iso.slice(0, 10);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
export const formatDateOnlyToPtBr = (value: string | Date): string => {
|
|
66
|
+
const yyyyMmDd = extractDateOnly(value);
|
|
67
|
+
if (!yyyyMmDd || !DATE_ONLY_REGEX.test(yyyyMmDd)) return "";
|
|
68
|
+
|
|
69
|
+
const [y, m, d] = yyyyMmDd.split("-").map(Number);
|
|
70
|
+
const day = String(d).padStart(2, "0");
|
|
71
|
+
const month = String(m).padStart(2, "0");
|
|
72
|
+
const year = String(y);
|
|
73
|
+
|
|
74
|
+
return `${day}/${month}/${year}`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const daysBetweenDateOnly = (start: string, end: string): number => {
|
|
78
|
+
const startStr = extractDateOnly(start);
|
|
79
|
+
const endStr = extractDateOnly(end);
|
|
80
|
+
if (!startStr || !endStr) return 0;
|
|
81
|
+
|
|
82
|
+
const startDate = new Date(startStr + "T12:00:00");
|
|
83
|
+
const endDate = new Date(endStr + "T12:00:00");
|
|
84
|
+
const diffMs = endDate.getTime() - startDate.getTime();
|
|
85
|
+
return Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const daysFromTodayToDateOnly = (isoOrDateStr: string): number => {
|
|
89
|
+
const endStr = extractDateOnly(isoOrDateStr);
|
|
90
|
+
if (!endStr) return 0;
|
|
91
|
+
|
|
92
|
+
const todayStr = getTodayDateString();
|
|
93
|
+
const todayNoon = new Date(todayStr + "T12:00:00");
|
|
94
|
+
const endNoon = new Date(endStr + "T12:00:00");
|
|
95
|
+
const diffMs = endNoon.getTime() - todayNoon.getTime();
|
|
96
|
+
return Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const formatDateToPtBr = (date: Date | string, onlyDate = false): string => {
|
|
100
|
+
const d = new Date(date);
|
|
101
|
+
if (isNaN(d.getTime())) return "";
|
|
102
|
+
|
|
103
|
+
if (onlyDate) {
|
|
104
|
+
return d.toLocaleDateString("pt-BR");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return d.toLocaleDateString("pt-BR") + " " + d.toLocaleTimeString("pt-BR", {
|
|
108
|
+
hour: "2-digit",
|
|
109
|
+
minute: "2-digit"
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const normalizePercentage = (
|
|
114
|
+
value: string | number | undefined,
|
|
115
|
+
fallback?: number
|
|
116
|
+
): number => {
|
|
117
|
+
if (value === undefined || value === null || value === "") {
|
|
118
|
+
return fallback ?? 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof value === "string") {
|
|
122
|
+
const numericValue = Number(value.replace(",", "."));
|
|
123
|
+
if (Number.isNaN(numericValue)) {
|
|
124
|
+
return fallback ?? 0;
|
|
125
|
+
}
|
|
126
|
+
return parsePercentageToFloat(numericValue);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (typeof value === "number") {
|
|
130
|
+
return parsePercentageToFloat(value);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return fallback ?? 0;
|
|
134
|
+
};
|
package/app/utils/percentage.ts
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Formata um número como porcentagem para exibição.
|
|
3
|
-
* Exibe casas decimais apenas quando o valor não for inteiro.
|
|
4
|
-
*
|
|
5
|
-
* @param value - Valor numérico da porcentagem (ex: 3.25, 50, 100)
|
|
6
|
-
* @returns String formatada com o símbolo % (ex: "3.25%", "50%", "100%")
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* formatPercentage(3.25) // "3.25%"
|
|
10
|
-
* formatPercentage(50) // "50%"
|
|
11
|
-
* formatPercentage(100) // "100%"
|
|
12
|
-
* formatPercentage(0) // "0%"
|
|
13
|
-
*/
|
|
14
|
-
export const formatPercentage = (value: number): string => {
|
|
15
|
-
// Se for inteiro, não mostra casas decimais
|
|
16
|
-
if (Number.isInteger(value)) {
|
|
17
|
-
return `${value}%`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Caso contrário, mostra 2 casas decimais
|
|
21
|
-
return `${value.toFixed(2)}%`;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Formata um número como porcentagem com um número específico de casas decimais.
|
|
26
|
-
*
|
|
27
|
-
* @param value - Valor numérico da porcentagem
|
|
28
|
-
* @param decimals - Número de casas decimais (padrão: 2)
|
|
29
|
-
* @returns String formatada com o símbolo %
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* formatPercentageWithDecimals(3.256, 2) // "3.26%"
|
|
33
|
-
* formatPercentageWithDecimals(50, 1) // "50.0%"
|
|
34
|
-
*/
|
|
35
|
-
export const formatPercentageWithDecimals = (
|
|
36
|
-
value: number,
|
|
37
|
-
decimals: number = 2
|
|
38
|
-
): string => {
|
|
39
|
-
return `${value.toFixed(decimals)}%`;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const formatPercentagePtBr = (
|
|
43
|
-
value: number | undefined | null,
|
|
44
|
-
decimals: number = 1
|
|
45
|
-
): string => {
|
|
46
|
-
if (value == null || Number.isNaN(value)) return "-";
|
|
47
|
-
|
|
48
|
-
const formatted = new Intl.NumberFormat("pt-BR", {
|
|
49
|
-
minimumFractionDigits: decimals,
|
|
50
|
-
maximumFractionDigits: decimals,
|
|
51
|
-
}).format(value);
|
|
52
|
-
|
|
53
|
-
return `${formatted}%`;
|
|
54
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Formata um número como porcentagem para exibição.
|
|
3
|
+
* Exibe casas decimais apenas quando o valor não for inteiro.
|
|
4
|
+
*
|
|
5
|
+
* @param value - Valor numérico da porcentagem (ex: 3.25, 50, 100)
|
|
6
|
+
* @returns String formatada com o símbolo % (ex: "3.25%", "50%", "100%")
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* formatPercentage(3.25) // "3.25%"
|
|
10
|
+
* formatPercentage(50) // "50%"
|
|
11
|
+
* formatPercentage(100) // "100%"
|
|
12
|
+
* formatPercentage(0) // "0%"
|
|
13
|
+
*/
|
|
14
|
+
export const formatPercentage = (value: number): string => {
|
|
15
|
+
// Se for inteiro, não mostra casas decimais
|
|
16
|
+
if (Number.isInteger(value)) {
|
|
17
|
+
return `${value}%`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Caso contrário, mostra 2 casas decimais
|
|
21
|
+
return `${value.toFixed(2)}%`;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Formata um número como porcentagem com um número específico de casas decimais.
|
|
26
|
+
*
|
|
27
|
+
* @param value - Valor numérico da porcentagem
|
|
28
|
+
* @param decimals - Número de casas decimais (padrão: 2)
|
|
29
|
+
* @returns String formatada com o símbolo %
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* formatPercentageWithDecimals(3.256, 2) // "3.26%"
|
|
33
|
+
* formatPercentageWithDecimals(50, 1) // "50.0%"
|
|
34
|
+
*/
|
|
35
|
+
export const formatPercentageWithDecimals = (
|
|
36
|
+
value: number,
|
|
37
|
+
decimals: number = 2
|
|
38
|
+
): string => {
|
|
39
|
+
return `${value.toFixed(decimals)}%`;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const formatPercentagePtBr = (
|
|
43
|
+
value: number | undefined | null,
|
|
44
|
+
decimals: number = 1
|
|
45
|
+
): string => {
|
|
46
|
+
if (value == null || Number.isNaN(value)) return "-";
|
|
47
|
+
|
|
48
|
+
const formatted = new Intl.NumberFormat("pt-BR", {
|
|
49
|
+
minimumFractionDigits: decimals,
|
|
50
|
+
maximumFractionDigits: decimals,
|
|
51
|
+
}).format(value);
|
|
52
|
+
|
|
53
|
+
return `${formatted}%`;
|
|
54
|
+
};
|