diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx index 2dfc2f4..33a1412 100644 --- a/src/components/ui/calendar.tsx +++ b/src/components/ui/calendar.tsx @@ -1,64 +1,159 @@ -import * as React from "react" -import { ChevronLeft, ChevronRight } from "lucide-react" -import { DayPicker } from "react-day-picker" +import * as React from 'react' +import { ChevronLeft, ChevronRight } from 'lucide-react' +import { DayPicker } from 'react-day-picker' -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/button' +import { format, isSameDay } from 'date-fns' +import { HoverCard, HoverCardContent, HoverCardTrigger } from './hover-card' +import clsx from 'clsx' +import { Link } from '@tanstack/react-router' +import { Badge } from './badge' +import { Separator } from '@radix-ui/react-separator' -export type CalendarProps = React.ComponentProps +/* eslint-disable-next-line */ +export type TData = { + type: TType + creditId: number + client: string + cuete: number +} + +/* eslint-disable-next-line */ +export type TDaysProps = { [date: string]: TData } + +export type CalendarProps = React.ComponentProps & { + days?: TDaysProps +} + +type TType = 'mora' | 'warning' function Calendar({ className, classNames, showOutsideDays = true, + days, ...props }: CalendarProps) { + return ( , IconRight: (props) => , + Day: !days ? undefined : (({ date }) => ( + + )), }} {...props} /> ) } -Calendar.displayName = "Calendar" + +/* eslint-disable-next-line */ +interface THoverDate { + date: Date + credit?: TData +} + +/* eslint-disable-next-line */ +function HoverDate({ date, credit }: THoverDate) { + if (!credit) + return {format(date, 'dd')} + return ( + + + + {' '} + {format(date, 'dd')} + + + +
+ + {' '} + {text.title({ type: credit.type })} + + {credit.creditId} +
+ +
    +
  • + {' '} + {text.list.client + ':'} {credit.client}{' '} +
  • +
  • + {' '} + {text.list.ammount + ':'} {credit.cuete}{' '} +
  • +
+
+
+ ) +} + +Calendar.displayName = 'Calendar' export { Calendar } + +const text = { + title: ({ type }: { type: TType }) => + 'Credito ' + (type === 'warning' ? 'Pendiente' : 'en Mora'), + list: { + client: 'Cliente', + ammount: 'Numero de cuota', + }, +} diff --git a/src/components/ui/loader.tsx b/src/components/ui/loader.tsx index 583fadf..7b5f342 100644 --- a/src/components/ui/loader.tsx +++ b/src/components/ui/loader.tsx @@ -1,15 +1,30 @@ import clsx from 'clsx' import * as React from 'react' -import styles from "@/components/ui/loader.module.css" +import styles from '@/components/ui/loader.module.css' interface TSpinLoader extends React.HTMLAttributes {} -const SpinLoader = React.forwardRef< HTMLSpanElement, TSpinLoader>(( { className, ...props }, ref) => ( - -)) +const SpinLoader = React.forwardRef( + ({ className, ...props }, ref) => ( + + ) +) -const BoundleLoader = React.forwardRef(( { className, ...props }, ref) => ( - +const BoundleLoader = React.forwardRef< + HTMLSpanElement, + React.PropsWithChildren +>(({ className, children, ...props }, ref) => ( + + {children} + )) export { SpinLoader, BoundleLoader } diff --git a/src/index.css b/src/index.css index 9d99bd6..d478496 100644 --- a/src/index.css +++ b/src/index.css @@ -37,8 +37,8 @@ --popover-foreground: 210 40% 98%; --primary: 207 55% 43%; --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 40 100% 35%; - --secondary-foreground: 40 100% 25%; + --secondary: 40 100% 30%; + --secondary-foreground: 40 85% 20%; --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; --accent: 235 26% 67%; diff --git a/src/mocks/data.ts b/src/mocks/data.ts index 2a67b63..cc6345d 100644 --- a/src/mocks/data.ts +++ b/src/mocks/data.ts @@ -6,13 +6,13 @@ import { listStatus } from '@/lib/type/status' import { listIds } from '@/lib/type/id' import { listMoraTypes } from '@/lib/type/moraType' import { listFrecuencys } from '@/lib/type/frecuency' -import { TROLES, listRols } from '@/lib/type/rol' -import { formatISO } from 'date-fns' +import { type TROLES, listRols } from '@/lib/type/rol' +import { add } from 'date-fns' import { faker } from '@faker-js/faker/locale/en' -import { TPAYMENT_GET_BASE } from '@/api/payment' +import { type TPAYMENT_GET_BASE } from '@/api/payment' const CLIENTS_LENGTH = 20 -const USERS_LENGTH = 10 +const USERS_LENGTH = 12 const CREDITS_LENGTH = 20 const REPORTS_LENGTH = 5 @@ -102,7 +102,7 @@ export const credits = new Map( multipleOf: 5.25, }) const aprobeDate = faker.date.between({ - from: new Date('2020-01-01'), + from: new Date('2024'), to: new Date(), }) const aditionalDays = faker.number.int(10) @@ -120,9 +120,10 @@ export const credits = new Map( refDate: aprobeDate, days: index + 1 + index + 1 * faker.number.int(index + 1), }) - const moraDate = new Date(payDate) - moraDate.setDate(payDate.getDate() + aditionalDays) + + const moraDate = add(new Date(payDate), { days: aditionalDays }) const cuoteAmmount = Math.ceil(ammount / cuotesLength) + const payMora = faker.helpers.maybe( () => mora.nombre === 'Valor fijo' @@ -140,10 +141,10 @@ export const credits = new Map( id: index + 1, numero_de_cuota: _id, credito_id: id, - fecha_de_pago: formatISO(payDate), + fecha_de_pago: payDate.toISOString(), valor_de_cuota: cuoteAmmount, valor_pagado: pay, - fecha_de_aplicacion_de_mora: formatISO(moraDate), + fecha_de_aplicacion_de_mora: moraDate.toISOString(), valor_de_mora: !!payMora && index < paymentsLength ? payMora : 0, pagada: index <= paymentsLength ? true : false, @@ -186,7 +187,7 @@ export const credits = new Map( return [ id, { - fecha_de_aprobacion: formatISO(aprobeDate), + fecha_de_aprobacion: aprobeDate.toISOString(), id, owner_id: faker.helpers.arrayElement([...clients?.values()])?.id ?? 1, monto: ammount, diff --git a/src/pages/-calendar.tsx b/src/pages/-calendar.tsx new file mode 100644 index 0000000..8c6bdd7 --- /dev/null +++ b/src/pages/-calendar.tsx @@ -0,0 +1,88 @@ +import { Badge } from "@/components/ui/badge" +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card" +import { Separator } from "@/components/ui/separator" +import { Link } from "@tanstack/react-router" +import clsx from "clsx" +import { format } from "date-fns" + +/* eslint-disable-next-line */ +type TType = 'mora' | 'warning' + +/* eslint-disable-next-line */ +export type TData = { + type: TType + creditId: number + client: string + cuete: number +} + +/* eslint-disable-next-line */ +export type TDaysProps = { [date: string]: TData } + +/* eslint-disable-next-line */ +interface THoverDate { + date: Date + credit?: TData +} + +/* eslint-disable-next-line */ +function HoverDate({ date, credit }: THoverDate) { + if (!credit) + return {format(date, 'dd')} + return ( + + + + {' '} + {format(date, 'dd')} + + + +
+ + {' '} + {text.title({ type: credit.type })} + + {credit.creditId} +
+ +
    +
  • + {' '} + {text.list.client + ':'} {credit.client}{' '} +
  • +
  • + {' '} + {text.list.ammount + ':'} {credit.cuete}{' '} +
  • +
+
+
+ ) +} + +export { HoverDate } + +const text = { + title: ({ type }: { type: TType }) => + 'Credito ' + (type === 'warning' ? 'Pendiente' : 'en Mora'), + list: { + client: 'Cliente', + ammount: 'Numero de cuota', + }, +} diff --git a/src/pages/-info.tsx b/src/pages/-info.tsx index 4dbb5e7..7ac2ceb 100644 --- a/src/pages/-info.tsx +++ b/src/pages/-info.tsx @@ -246,6 +246,7 @@ export const MyUserInfo = memo(function () { defaultValue={userRes?.nombre.split(' ')?.at(0)} onChange={onChangeName('firstName')} disabled={!!userId && rol?.rolName !== 'Administrador'} + pattern="^[a-zA-Z]+(?: [a-zA-Z]+)?$" /> )} @@ -262,6 +263,7 @@ export const MyUserInfo = memo(function () { defaultValue={userRes?.nombre.split(' ')?.at(1)} onChange={onChangeName('lastName')} disabled={!!userId && rol?.rolName !== 'Administrador'} + pattern="^[a-zA-Z]+(?: [a-zA-Z]+)?$" /> )} @@ -318,6 +320,7 @@ export const MyUserInfo = memo(function () { placeholder={text.form.password.current.placeholder} defaultValue={password?.password} onChange={onChangePassword} + pattern="^.{6,}$" /> )} @@ -351,6 +354,7 @@ export const MyUserInfo = memo(function () { placeholder={text.form.password.new.placeholder} defaultValue={password?.confirmation} onChange={onChangePassword} + pattern={password?.password} /> )} diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index b8f3322..38686ba 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -23,7 +23,7 @@ import { import { Separator } from '@/components/ui/separator' import { Link } from '@tanstack/react-router' import { Button } from '@/components/ui/button' -import { Calendar } from '@/components/ui/calendar' +import { Calendar, TData, TDaysProps } from '@/components/ui/calendar' import React, { memo, useEffect, useReducer, useState } from 'react' import { User } from 'lucide-react' import { Avatar, AvatarFallback } from '@/components/ui/avatar' @@ -104,6 +104,8 @@ import { getRolByName, TROLES } from '@/lib/type/rol' import { translate } from '@/lib/route' import { Dialog, DialogTrigger } from '@/components/ui/dialog' import { MyUserInfo } from '@/pages/-info' +import { format } from 'date-fns' +import { TCREDIT_GET_FILTER_ALL } from '@/api/credit' export const getCurrentUserOpt = { queryKey: ['login-user', { userId: useToken.getState().userId }], @@ -116,6 +118,9 @@ export const Route = createFileRoute('/_layout')({ loader: async () => ({ user: queryClient.ensureQueryData(queryOptions(getCurrentUserOpt)), clients: defer(queryClient.ensureQueryData(queryOptions(getClientListOpt))), + credits: defer( + queryClient.ensureQueryData(queryOptions(getCreditsListOpt)) + ), }), onLeave: () => { useToken.setState({ @@ -169,7 +174,7 @@ export function Layout() { refetch, } = useSuspenseQuery(queryOptions(getCurrentUserOpt)) - const select: (data: TCLIENT_GET_ALL) => TCLIENT_GET_ALL = (data) => { + const selectClients: (data: TCLIENT_GET_ALL) => TCLIENT_GET_ALL = (data) => { const clients = data if (userId && rol?.rolName !== 'Administrador') return clients?.filter(({ owner_id }) => owner_id === userId) @@ -181,7 +186,39 @@ export function Layout() { isSuccess: okClients, error: errorClients, isPending: _pendingClients, - } = useQuery(queryOptions({ ...getClientListOpt, select })) + } = useQuery(queryOptions({ ...getClientListOpt, select: selectClients })) + + const selectCredits: (data: TCREDIT_GET_FILTER_ALL) => TDaysProps = ( + data + ) => { + let credits: TCREDIT_GET_FILTER_ALL = data + if (userId && rol?.rolName !== 'Administrador') + credits = data?.filter(({ cobrador_id }) => cobrador_id === userId) + + return Object.fromEntries( + credits?.map<[string, TData]>( + ({ + fecha_de_cuota, + id: creditId, + nombre_del_cliente, + valor_de_la_mora, + numero_de_cuota, + }) => [ + format(fecha_de_cuota, 'dd-MM-yyyy'), + { + type: valor_de_la_mora > 0 ? 'mora' : 'warning', + creditId, + client: nombre_del_cliente, + cuete: numero_de_cuota, + }, + ] + ) + ) + } + + const { data: creditssRes } = useQuery( + queryOptions({ ...getCreditsListOpt, select: selectCredits }) + ) const [clients, setClients] = useState(undefined) const { theme, setTheme } = useTheme() @@ -369,7 +406,8 @@ export function Layout() { {!menu ? ( ) : ( @@ -385,6 +423,7 @@ export function Layout() { diff --git a/src/pages/_layout/client/$clientId/update.tsx b/src/pages/_layout/client/$clientId/update.tsx index 0c85319..7c654a3 100644 --- a/src/pages/_layout/client/$clientId/update.tsx +++ b/src/pages/_layout/client/$clientId/update.tsx @@ -167,7 +167,7 @@ export function UpdateClientById() { } }, [clientRes]) - const clientsList = useContext(_clientContext) + const clients = useContext(_clientContext) const active = useMemo( () => @@ -188,7 +188,7 @@ export function UpdateClientById() { ) const ref = useMemo( - () => clientsList?.find(({ id: refId }) => refId === client?.referencia_id), + () => clients?.find(({ id: refId }) => refId === client?.referencia_id), [client] ) @@ -237,7 +237,7 @@ export function UpdateClientById() { if (!name || !value || !client) return if (name === ('referencia' as TFormName)) { - const refId = clientsList?.find(({ fullName }) => value === fullName)?.id + const refId = clients?.find(({ fullName }) => value === fullName)?.id setClient({ ...client, referencia_id: refId }) return } @@ -282,6 +282,7 @@ export function UpdateClientById() { name={'nombres' as TFormName} type="text" defaultValue={clientRes?.nombres} + pattern="^[a-zA-Z]+(?: [a-zA-Z]+)?$" placeholder={ checked ? text.form.firstName.placeholder : undefined } @@ -299,6 +300,7 @@ export function UpdateClientById() { name={'apellidos' as TFormName} type="text" defaultValue={clientRes?.apellidos} + pattern="^[a-zA-Z]+(?: [a-zA-Z]+)?$" placeholder={ checked ? text.form.lastName.placeholder : undefined } @@ -317,6 +319,7 @@ export function UpdateClientById() { type="text" defaultValue={clientRes?.numero_de_identificacion} placeholder={checked ? text.form.id.placeholder : undefined} + pattern="[A-Za-z0-9]{6,12}" /> )} @@ -357,6 +360,7 @@ export function UpdateClientById() { name={'celular' as TFormName} type="tel" defaultValue={clientRes?.celular} + pattern="(?:\+57|0)[0-9]{8}" placeholder={checked ? text.form.phone.placeholder : undefined} /> )} @@ -372,6 +376,7 @@ export function UpdateClientById() { name={'telefono' as TFormName} type="tel" defaultValue={clientRes?.telefono} + pattern="(?:\+57|0)[0-9]{6,7}" placeholder={ checked ? text.form.telephone.placeholder : undefined } @@ -408,9 +413,12 @@ export function UpdateClientById() { type="text" defaultValue={ref?.fullName} placeholder={checked ? text.form.ref.placeholder : undefined} + pattern={`(${clients + ?.map(({ fullName }) => fullName?.replace(/\s+/g, '\\s+')) + ?.join('|')})`} /> - {clientsList?.map(({ fullName }, index) => ( + {clients?.map(({ fullName }, index) => ( diff --git a/src/pages/_layout/client/new.tsx b/src/pages/_layout/client/new.tsx index b72b6e7..93a9d06 100644 --- a/src/pages/_layout/client/new.tsx +++ b/src/pages/_layout/client/new.tsx @@ -16,8 +16,18 @@ import { useContext, useRef } from 'react' import styles from '@/styles/global.module.css' import clsx from 'clsx' import { ToastAction } from '@radix-ui/react-toast' -import { postClient, type TCLIENT_POST, type TCLIENT_POST_BODY } from "@/api/clients"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { + postClient, + type TCLIENT_POST, + type TCLIENT_POST_BODY, +} from '@/api/clients' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' import { useNotifications } from '@/lib/context/notification' import { useMutation } from '@tanstack/react-query' import { listIds, getIdByName } from '@/lib/type/id' @@ -27,7 +37,7 @@ import { getStatusByName } from '@/lib/type/status' import { Textarea } from '@/components/ui/textarea' export const postClientOpt = { - mutationKey: ["create-client"], + mutationKey: ['create-client'], mutationFn: postClient, } @@ -36,7 +46,8 @@ export const Route = createFileRoute('/_layout/client/new')({ }) /* eslint-disable-next-line */ -type TFormName = keyof (Omit & Record<"referencia", string>) +type TFormName = keyof (Omit & + Record<'referencia', string>) /* eslint-disable-next-line */ export function NewClient() { @@ -44,7 +55,10 @@ export function NewClient() { const { pushNotification } = useNotifications() const { open } = useStatus() - const onSuccess: (data: TCLIENT_POST, variables: TCLIENT_POST_BODY) => unknown = (_, items) => { + const onSuccess: ( + data: TCLIENT_POST, + variables: TCLIENT_POST_BODY + ) => unknown = (_, items) => { const description = text.notification.decription({ username: items?.nombres + items?.apellidos, }) @@ -57,12 +71,15 @@ export function NewClient() { pushNotification({ date: new Date(), - action: "POST", + action: 'POST', description, }) } - const onError: (data: TCLIENT_POST, variables: TCLIENT_POST_BODY) => unknown = (_, items) => { + const onError: ( + data: TCLIENT_POST, + variables: TCLIENT_POST_BODY + ) => unknown = (_, items) => { const description = text.notification.error({ username: items?.nombres + items?.apellidos, }) @@ -75,28 +92,31 @@ export function NewClient() { variant: 'destructive', action: ( - {text.notification.retry} + {text.notification.retry} ), }) - } - const {mutate: createClient} = useMutation( { ...postClientOpt, + const { mutate: createClient } = useMutation({ + ...postClientOpt, onSuccess, - onError - } ) + onError, + }) const clients = useContext(_clientContext) const onSubmit: React.FormEventHandler = (ev) => { if (!form.current) return const items = Object.fromEntries( - Array.from(new FormData(form.current).entries())?.map( ([ key, value ]) => { - if( value === "" ) return [ key, undefined ] - return [ key, value ] - })) as Record + Array.from(new FormData(form.current).entries())?.map(([key, value]) => { + if (value === '') return [key, undefined] + return [key, value] + }) + ) as Record - const refId = clients?.find( ({ fullName }) => ( items?.referencia === fullName ) )?.id + const refId = clients?.find( + ({ fullName }) => items?.referencia === fullName + )?.id createClient({ nombres: items?.nombres, apellidos: items?.apellidos, @@ -105,11 +125,11 @@ export function NewClient() { celular: items?.celular, numero_de_identificacion: items?.numero_de_identificacion, tipo_de_identificacion: +items?.tipo_de_identificacion, - estado: getStatusByName({ statusName: "Activo" })?.id, + estado: getStatusByName({ statusName: 'Activo' })?.id, referencia_id: refId ?? null, - comentarios: items?.comentarios ?? "", + comentarios: items?.comentarios ?? '', // TODO: this field be "" that not's necessary - email: items?.email ?? "", + email: items?.email ?? '', }) form.current.reset() @@ -118,127 +138,144 @@ export function NewClient() { return ( <> - { !open && } - - - {text.title} - - {text.descriiption} - -
label]:space-y-2', - styles?.['custom-form'] - )} - > - - - - - - - - -