Improve custom field timeline and toasts
This commit is contained in:
parent
f7aa17f229
commit
a2f9d4bd1a
9 changed files with 549 additions and 101 deletions
|
|
@ -20,6 +20,36 @@ function normalizeTicketStatus(status: unknown): NormalizedTicketStatus {
|
|||
return normalized ?? "PENDING";
|
||||
}
|
||||
|
||||
const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
function normalizeCustomFieldDateValue(raw: unknown): string | null {
|
||||
if (raw === null || raw === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (typeof raw === "string") {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return null;
|
||||
if (DATE_ONLY_REGEX.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
const parsed = new Date(trimmed);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return null;
|
||||
}
|
||||
return parsed.toISOString().slice(0, 10);
|
||||
}
|
||||
const date =
|
||||
raw instanceof Date
|
||||
? raw
|
||||
: typeof raw === "number"
|
||||
? new Date(raw)
|
||||
: null;
|
||||
if (!date || Number.isNaN(date.getTime())) {
|
||||
return null;
|
||||
}
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
// Server shapes: datas como number (epoch ms) e alguns nullables
|
||||
// Relaxamos email/urls no shape do servidor para evitar que payloads parciais quebrem o app.
|
||||
const serverUserSchema = z.object({
|
||||
|
|
@ -246,8 +276,8 @@ export function mapTicketWithDetailsFromServer(input: unknown) {
|
|||
>(
|
||||
(acc, [key, value]) => {
|
||||
let parsedValue: unknown = value.value;
|
||||
if (value.type === "date" && typeof value.value === "number") {
|
||||
parsedValue = new Date(value.value);
|
||||
if (value.type === "date") {
|
||||
parsedValue = normalizeCustomFieldDateValue(value.value) ?? value.value;
|
||||
}
|
||||
acc[key] = {
|
||||
label: value.label,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { format } from "date-fns"
|
||||
import { format, parse } from "date-fns"
|
||||
import { ptBR } from "date-fns/locale"
|
||||
|
||||
export type TicketCustomFieldRecord = Record<
|
||||
|
|
@ -25,18 +25,41 @@ function formatBoolean(value: unknown): string {
|
|||
return value === true || String(value).toLowerCase() === "true" ? "Sim" : "Não"
|
||||
}
|
||||
|
||||
const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/
|
||||
|
||||
function normalizeIsoDate(value: unknown): string | null {
|
||||
if (value === null || value === undefined) return null
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return null
|
||||
if (DATE_ONLY_REGEX.test(trimmed)) {
|
||||
return trimmed
|
||||
}
|
||||
const parsed = new Date(trimmed)
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return null
|
||||
}
|
||||
return parsed.toISOString().slice(0, 10)
|
||||
}
|
||||
const date =
|
||||
value instanceof Date
|
||||
? value
|
||||
: typeof value === "number" && Number.isFinite(value)
|
||||
? new Date(value)
|
||||
: null
|
||||
if (!date || Number.isNaN(date.getTime())) {
|
||||
return null
|
||||
}
|
||||
return date.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function formatDate(value: unknown): string {
|
||||
if (value === null || value === undefined) return "—"
|
||||
const date =
|
||||
typeof value === "number"
|
||||
? Number.isFinite(value)
|
||||
? new Date(value)
|
||||
: null
|
||||
: typeof value === "string" && value.trim().length > 0
|
||||
? new Date(value)
|
||||
: null
|
||||
if (!date || Number.isNaN(date.getTime())) return "—"
|
||||
return format(date, "dd/MM/yyyy", { locale: ptBR })
|
||||
const iso = normalizeIsoDate(value)
|
||||
if (!iso) return "—"
|
||||
const parsed = parse(iso, "yyyy-MM-dd", new Date())
|
||||
if (Number.isNaN(parsed.getTime())) return "—"
|
||||
return format(parsed, "dd/MM/yyyy", { locale: ptBR })
|
||||
}
|
||||
|
||||
function formatNumber(value: unknown): string {
|
||||
|
|
|
|||
71
src/lib/toast-patch.ts
Normal file
71
src/lib/toast-patch.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
"use client"
|
||||
|
||||
import { toast } from "sonner"
|
||||
|
||||
const METHODS = ["success", "error", "info", "warning", "message", "loading"] as const
|
||||
const TRAILING_PUNCTUATION_REGEX = /[\s!?.…,;:]+$/u
|
||||
const toastAny = toast as unknown as Record<string, (...args: any[]) => unknown> & { __punctuationPatched?: boolean }
|
||||
|
||||
function stripTrailingPunctuation(value: string): string {
|
||||
const trimmed = value.trimEnd()
|
||||
if (!trimmed) return trimmed
|
||||
return trimmed.replace(TRAILING_PUNCTUATION_REGEX, "").trimEnd()
|
||||
}
|
||||
|
||||
function sanitizeContent<T>(value: T): T {
|
||||
if (typeof value === "string") {
|
||||
return stripTrailingPunctuation(value) as T
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function sanitizeOptions(options: unknown): unknown {
|
||||
if (!options || typeof options !== "object") return options
|
||||
if ("description" in options && typeof (options as { description?: unknown }).description === "string") {
|
||||
return {
|
||||
...(options as Record<string, unknown>),
|
||||
description: stripTrailingPunctuation((options as { description: string }).description),
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
function wrapSimpleMethod(method: (typeof METHODS)[number]) {
|
||||
const original = toastAny[method]
|
||||
if (typeof original !== "function") return
|
||||
toastAny[method] = (message: unknown, options?: unknown, ...rest: unknown[]) => {
|
||||
return original(sanitizeContent(message), sanitizeOptions(options), ...rest)
|
||||
}
|
||||
}
|
||||
|
||||
function wrapPromise() {
|
||||
const originalPromise = toastAny.promise
|
||||
if (typeof originalPromise !== "function") return
|
||||
toastAny.promise = (promise: Promise<unknown>, messages: Record<string, unknown>, options?: unknown) => {
|
||||
const wrapMessage = (value: unknown) => {
|
||||
if (typeof value === "function") {
|
||||
return (...args: unknown[]) => sanitizeContent((value as (...args: unknown[]) => unknown)(...args))
|
||||
}
|
||||
return sanitizeContent(value)
|
||||
}
|
||||
|
||||
const normalizedMessages =
|
||||
messages && typeof messages === "object"
|
||||
? {
|
||||
...messages,
|
||||
loading: wrapMessage(messages.loading),
|
||||
success: wrapMessage(messages.success),
|
||||
error: wrapMessage(messages.error),
|
||||
finally: wrapMessage((messages as { finally?: unknown }).finally),
|
||||
}
|
||||
: messages
|
||||
|
||||
return originalPromise(promise, normalizedMessages, sanitizeOptions(options))
|
||||
}
|
||||
}
|
||||
|
||||
if (!toastAny.__punctuationPatched) {
|
||||
toastAny.__punctuationPatched = true
|
||||
METHODS.forEach(wrapSimpleMethod)
|
||||
wrapPromise()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue