.
This commit is contained in:
parent
eb3e2ebc47
commit
247d13ffcf
@ -13,7 +13,6 @@ export const StartPage = () => {
|
|||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
to={redirectTo ?? "/login"}
|
to={redirectTo ?? "/login"}
|
||||||
replace
|
|
||||||
state={{
|
state={{
|
||||||
error: "No authentication, please complete the login process.",
|
error: "No authentication, please complete the login process.",
|
||||||
}}
|
}}
|
||||||
@ -21,5 +20,5 @@ export const StartPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Navigate to={"/quotes"} replace />;
|
return <Navigate to={"/quotes"} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -64,9 +64,9 @@ export const LoginPage = () => {
|
|||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
variant={"full"}
|
variant={"full"}
|
||||||
className='p-0 lg:grid lg:min-h-[600px] lg:grid-cols-2 xl:min-h-[800px] h-screen'
|
className='p-0 lg:grid lg:min-h-[600px] lg:grid-cols-2 xl:min-h-[800px] h-screen '
|
||||||
>
|
>
|
||||||
<div className='flex items-center justify-center py-12'>
|
<div className='flex items-center justify-center md:py-12'>
|
||||||
<div className='mx-auto grid w-[650px] gap-6'>
|
<div className='mx-auto grid w-[650px] gap-6'>
|
||||||
<Card className='px-12 py-6'>
|
<Card className='px-12 py-6'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@ -72,7 +72,7 @@ export const QuoteCreate = () => {
|
|||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
reset(getValues());
|
reset(getValues());
|
||||||
toast.success("Cotización guardada");
|
toast.success("Cotización guardada");
|
||||||
navigate(`/quotes/edit/${data.id}`, { relative: "path", replace: true });
|
navigate(`/quotes/edit/${data.id}`, { relative: "path" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -2,13 +2,16 @@ import { DataTableProvider } from "@/lib/hooks";
|
|||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
import { QuotesDataTable } from "./components";
|
import { QuotesDataTable } from "./components";
|
||||||
|
|
||||||
|
import { HelpButton } from "@/components";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Label,
|
||||||
DialogContent,
|
ScrollArea,
|
||||||
DialogHeader,
|
Select,
|
||||||
DialogTitle,
|
SelectContent,
|
||||||
DialogTrigger,
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
TabsList,
|
TabsList,
|
||||||
@ -17,13 +20,25 @@ import {
|
|||||||
} from "@/ui";
|
} from "@/ui";
|
||||||
import { useToggle } from "@wojtekmaj/react-hooks";
|
import { useToggle } from "@wojtekmaj/react-hooks";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { EyeIcon, EyeOffIcon, HelpCircleIcon, PlusIcon } from "lucide-react";
|
import { EyeIcon, EyeOffIcon, PlusIcon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export const QuotesList = () => {
|
export const QuotesList = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [status, setStatus] = useState("all");
|
||||||
const [enabledPreview, toggleEnabledPreview] = useToggle(true);
|
const [enabledPreview, toggleEnabledPreview] = useToggle(true);
|
||||||
|
|
||||||
|
const quoteStatuses = [
|
||||||
|
{ value: "all", label: t("quotes.list.tabs.all") },
|
||||||
|
{ value: "draft", label: t("quotes.list.tabs.draft") },
|
||||||
|
{ value: "ready", label: t("quotes.list.tabs.ready") },
|
||||||
|
{ value: "delivered", label: t("quotes.list.tabs.delivered") },
|
||||||
|
{ value: "accepted", label: t("quotes.list.tabs.delivered") },
|
||||||
|
{ value: "rejected", label: t("quotes.list.tabs.rejected") },
|
||||||
|
{ value: "archived", label: t("quotes.list.tabs.archived") },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTableProvider>
|
<DataTableProvider>
|
||||||
<div className='flex items-center justify-between space-y-2'>
|
<div className='flex items-center justify-between space-y-2'>
|
||||||
@ -43,83 +58,38 @@ export const QuotesList = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs defaultValue='all'>
|
<Tabs value={status} onValueChange={setStatus}>
|
||||||
<div className='flex items-baseline'>
|
<div className='flex flex-col items-start justify-between mb-4 sm:flex-row sm:items-center'>
|
||||||
<TabsList>
|
<div className='w-full mb-4 sm:w-auto sm:mb-0'>
|
||||||
<TabsTrigger value='all'>
|
<TabsList className='hidden sm:flex'>
|
||||||
<Trans i18nKey='quotes.list.tabs.all' />
|
{quoteStatuses.map((s) => (
|
||||||
</TabsTrigger>
|
<TabsTrigger key={s.value} value={s.value}>
|
||||||
<TabsTrigger value='draft'>
|
{s.label}
|
||||||
<Trans i18nKey='quotes.list.tabs.draft' />
|
</TabsTrigger>
|
||||||
</TabsTrigger>
|
))}
|
||||||
<TabsTrigger value='ready'>
|
</TabsList>
|
||||||
<Trans i18nKey='quotes.list.tabs.ready' />
|
<div className='flex items-center w-full space-x-2 sm:hidden'>
|
||||||
</TabsTrigger>
|
<Label>{t("quotes.list.tabs_title")}</Label>
|
||||||
<TabsTrigger value='delivered'>
|
<Select value={status} onValueChange={setStatus}>
|
||||||
<Trans i18nKey='quotes.list.tabs.delivered' />
|
<SelectTrigger>
|
||||||
</TabsTrigger>
|
<SelectValue placeholder='Seleccionar estado' />
|
||||||
<TabsTrigger value='accepted'>
|
</SelectTrigger>
|
||||||
<Trans i18nKey='quotes.list.tabs.accepted' />
|
<SelectContent>
|
||||||
</TabsTrigger>
|
{quoteStatuses.map((s) => (
|
||||||
<TabsTrigger value='rejected'>
|
<SelectItem key={s.value} value={s.value}>
|
||||||
<Trans i18nKey='quotes.list.tabs.rejected' />
|
{s.label}
|
||||||
</TabsTrigger>
|
</SelectItem>
|
||||||
<TabsTrigger value='archived' className='hidden sm:flex'>
|
))}
|
||||||
<Trans i18nKey='quotes.list.tabs.archived' />
|
</SelectContent>
|
||||||
</TabsTrigger>
|
</Select>
|
||||||
</TabsList>
|
</div>
|
||||||
|
</div>
|
||||||
<div className='flex items-baseline justify-center mr-4 font-medium'>
|
<div className='flex items-baseline justify-center mr-4 font-medium'>
|
||||||
<Dialog>
|
<HelpButton
|
||||||
<DialogTrigger asChild>
|
buttonText='Ayuda'
|
||||||
<Button variant='link' className='inline-flex items-center font-medium group'>
|
title='Ayuda sobre el Estado de Cotizaciones'
|
||||||
<Trans
|
content={HelpContent}
|
||||||
i18nKey='quotes.list.tabs_title'
|
/>
|
||||||
className='underline-offset-4 group-hover:underline'
|
|
||||||
/>
|
|
||||||
<HelpCircleIcon className='w-4 h-4 ml-1 text-muted-foreground' />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className='sm:max-w-[425px]'>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Ayuda sobre el Estado de Cotizaciones</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className='grid gap-4 py-4'>
|
|
||||||
<p>
|
|
||||||
El estado de una cotización indica su posición actual en el proceso de negocio.
|
|
||||||
</p>
|
|
||||||
<p>Los estados posibles son:</p>
|
|
||||||
<ul className='pl-6 space-y-2 list-disc'>
|
|
||||||
<li>
|
|
||||||
<strong>Borrador:</strong> La cotización está en proceso de creación o
|
|
||||||
edición.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Preparado:</strong> La cotización está lista para ser enviada al
|
|
||||||
cliente.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Entregado:</strong> La cotización ha sido enviada al cliente.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Aceptado:</strong> El cliente ha aprobado la cotización.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Rechazado:</strong> El cliente no ha aceptado la cotización.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Arcivado:</strong> La cotización ha sido guardada para referencia
|
|
||||||
futura y ya no está activa.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
Utiliza estos estados para hacer un seguimiento eficiente de tus cotizaciones y
|
|
||||||
optimizar tu proceso de ventas. El estado <strong>Archivado</strong> es útil
|
|
||||||
para mantener un historial de cotizaciones pasadas sin que interfieran con las
|
|
||||||
cotizaciones activas.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 ml-auto'>
|
<div className='flex items-center gap-2 ml-auto'>
|
||||||
<Toggle
|
<Toggle
|
||||||
@ -143,28 +113,45 @@ export const QuotesList = () => {
|
|||||||
</Toggle>
|
</Toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TabsContent value='all'>
|
{quoteStatuses.map((s) => (
|
||||||
<QuotesDataTable status='all' preview={enabledPreview} />
|
<TabsContent key={s.value} value={s.value}>
|
||||||
</TabsContent>
|
<QuotesDataTable status={s.value} preview={enabledPreview} />
|
||||||
<TabsContent value='draft'>
|
</TabsContent>
|
||||||
<QuotesDataTable status='draft' preview={enabledPreview} />
|
))}
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='ready'>
|
|
||||||
<QuotesDataTable status='ready' preview={enabledPreview} />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='delivered'>
|
|
||||||
<QuotesDataTable status='delivered' preview={enabledPreview} />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='accepted'>
|
|
||||||
<QuotesDataTable status='accepted' preview={enabledPreview} />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='rejected'>
|
|
||||||
<QuotesDataTable status='rejected' preview={enabledPreview} />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='archived'>
|
|
||||||
<QuotesDataTable status='archived' preview={enabledPreview} />
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</DataTableProvider>
|
</DataTableProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const HelpContent = (
|
||||||
|
<ScrollArea className='grid gap-4 py-4'>
|
||||||
|
<p>El estado de una cotización indica su posición actual en el proceso de negocio.</p>
|
||||||
|
<p>Los estados posibles son:</p>
|
||||||
|
<ul className='pl-6 space-y-2 list-disc'>
|
||||||
|
<li>
|
||||||
|
<strong>Borrador:</strong> La cotización está en proceso de creación o edición.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Preparado:</strong> La cotización está lista para ser enviada al cliente.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Entregado:</strong> La cotización ha sido enviada al cliente.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Aceptado:</strong> El cliente ha aprobado la cotización.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Rechazado:</strong> El cliente no ha aceptado la cotización.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Arcivado:</strong> La cotización ha sido guardada para referencia futura y ya no
|
||||||
|
está activa.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Utiliza estos estados para hacer un seguimiento eficiente de tus cotizaciones y optimizar tu
|
||||||
|
proceso de ventas. El estado <strong>Archivado</strong> es útil para mantener un historial de
|
||||||
|
cotizaciones pasadas sin que interfieran con las cotizaciones activas.
|
||||||
|
</p>
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
Form,
|
Form,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
} from "@/ui";
|
} from "@/ui";
|
||||||
|
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
@ -29,7 +31,7 @@ type SettingsDataForm = IUpdateProfile_Request_DTO;
|
|||||||
|
|
||||||
export const SettingsEditor = () => {
|
export const SettingsEditor = () => {
|
||||||
const [activeSection, setActiveSection] = useState("profile");
|
const [activeSection, setActiveSection] = useState("profile");
|
||||||
const { useOne, useUpdate } = useSettings();
|
const { useOne, useUpdate, useUploadLogo } = useSettings();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { data, status, error: queryError } = useOne();
|
const { data, status, error: queryError } = useOne();
|
||||||
@ -50,6 +52,7 @@ export const SettingsEditor = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { mutate } = useUpdate();
|
const { mutate } = useUpdate();
|
||||||
|
const { upload } = useUploadLogo();
|
||||||
|
|
||||||
const form = useForm<SettingsDataForm>({
|
const form = useForm<SettingsDataForm>({
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
@ -70,15 +73,46 @@ export const SettingsEditor = () => {
|
|||||||
),*/
|
),*/
|
||||||
});
|
});
|
||||||
|
|
||||||
const { formState, reset, getValues, handleSubmit } = form;
|
const { formState, reset, getValues, handleSubmit, setValue } = form;
|
||||||
const { isSubmitting, isDirty } = formState;
|
const { isSubmitting, isDirty } = formState;
|
||||||
|
|
||||||
|
const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
if (file.type !== "image/png" && file.type !== "image/jpeg") {
|
||||||
|
alert(t("settings.form_fields.logo.invalid_format"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
const width = img.width;
|
||||||
|
const height = img.height;
|
||||||
|
|
||||||
|
// Comprobar que la imagen tiene una proporción de 2:1 (ancho:alto)
|
||||||
|
const isProportional = width / height === 2;
|
||||||
|
|
||||||
|
// Comprobar que las dimensiones son iguales o mayores a 200x100 y proporcionales
|
||||||
|
if (width < 200 || height < 100 || !isProportional) {
|
||||||
|
alert(t("settings.form_fields.logo.invalid_dimensions"));
|
||||||
|
} else {
|
||||||
|
upload(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.src = reader.result as string;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useUnsavedChangesNotifier({
|
useUnsavedChangesNotifier({
|
||||||
isDirty,
|
isDirty,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<SettingsDataForm> = async (data) => {
|
const onSubmit: SubmitHandler<SettingsDataForm> = async (data) => {
|
||||||
console.log("hola");
|
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.debug(error);
|
console.debug(error);
|
||||||
@ -170,6 +204,51 @@ export const SettingsEditor = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
<Trans i18nKey='settings.form_fields.logo.label' />
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
<Trans i18nKey='settings.form_fields.logo.desc' />
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className='flex items-center space-x-4'>
|
||||||
|
<div className='w-[400px] h-[200px] border border-gray-300 flex items-center justify-center overflow-hidden'>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
data.dealer.logo ? data.dealer.logo : "https://via.placeholder.com/200x100"
|
||||||
|
}
|
||||||
|
width={400}
|
||||||
|
height={200}
|
||||||
|
style={{ objectFit: "contain" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className='mt-2 text-sm text-gray-500'>
|
||||||
|
<Trans i18nKey='settings.form_fields.logo.requirements' />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className='px-6 py-4 border-t'>
|
||||||
|
<Label
|
||||||
|
htmlFor='logo-upload'
|
||||||
|
className='inline-flex items-center justify-center h-10 px-4 py-2 text-sm font-medium transition-colors rounded-md cursor-pointer bg-primary text-primary-foreground hover:bg-primary/90 whitespace-nowrap ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'
|
||||||
|
>
|
||||||
|
<Trans i18nKey='settings.form_fields.logo.upload' />
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id='logo-upload'
|
||||||
|
type='file'
|
||||||
|
accept='.png,.jpg,.jpeg'
|
||||||
|
onChange={handleLogoChange}
|
||||||
|
className='hidden'
|
||||||
|
/>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn("grid gap-6", activeSection === "quotes" ? "visible" : "hidden")}>
|
<div className={cn("grid gap-6", activeSection === "quotes" ? "visible" : "hidden")}>
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export const useSettings = (params?: UseSettingsGetParamsType) => {
|
|||||||
}),
|
}),
|
||||||
...params,
|
...params,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
useUpdate: () => {
|
useUpdate: () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@ -48,5 +49,40 @@ export const useSettings = (params?: UseSettingsGetParamsType) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
useUploadLogo: () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { mutate, mutateAsync, ...rest } = useMutation<
|
||||||
|
any, //IUploadProfileLogoResponse_DTO,
|
||||||
|
TDataSourceError,
|
||||||
|
any
|
||||||
|
>({
|
||||||
|
mutationKey: ["data", "default", "settings"], //keys().data().resource("settings").action("upload").id("me").params().get(),
|
||||||
|
mutationFn: (data) => {
|
||||||
|
return dataSource.uploadFile({
|
||||||
|
path: "profile/logo",
|
||||||
|
//resource: "profile",
|
||||||
|
//id: "",
|
||||||
|
file: data,
|
||||||
|
key: "logo",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
return queryClient.invalidateQueries({
|
||||||
|
queryKey: ["data", "default", "settings"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["data", "default", "settings"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
upload: mutate,
|
||||||
|
uploadAsync: mutateAsync,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
54
client/src/components/Buttons/HelpButton.tsx
Normal file
54
client/src/components/Buttons/HelpButton.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
ScrollArea,
|
||||||
|
} from "@/ui";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { HelpCircleIcon } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface HelpButtonProps {
|
||||||
|
buttonText: string;
|
||||||
|
title?: string;
|
||||||
|
content: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HelpButton = ({
|
||||||
|
buttonText,
|
||||||
|
title = "",
|
||||||
|
content,
|
||||||
|
className = "",
|
||||||
|
}: HelpButtonProps) => {
|
||||||
|
return (
|
||||||
|
<div className={`flex items-baseline justify-center mr-4 font-medium ${className}`}>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant='link' className='inline-flex items-center font-medium group'>
|
||||||
|
<span className='underline-offset-4 group-hover:underline'>{buttonText}</span>
|
||||||
|
<HelpCircleIcon className='w-4 h-4 ml-1 text-muted-foreground' />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className='sm:max-w-[425px]'>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<ScrollArea className='grid gap-4 py-2'>
|
||||||
|
{content}
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type='button'>{t("common.close")}</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</ScrollArea>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./CancelButton";
|
export * from "./CancelButton";
|
||||||
|
export * from "./HelpButton";
|
||||||
export * from "./SubmitButton";
|
export * from "./SubmitButton";
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { useGetProfile, useIsLoggedIn } from "@/lib/hooks";
|
import { useIsLoggedIn } from "@/lib/hooks";
|
||||||
import React, { useEffect, useState } from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
import { LoadingOverlay } from "../LoadingOverlay";
|
|
||||||
|
|
||||||
type ProctectRouteProps = {
|
type ProctectRouteProps = {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@ -10,9 +8,9 @@ type ProctectRouteProps = {
|
|||||||
|
|
||||||
export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
|
export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
|
||||||
const { isPending, isSuccess, data: { authenticated, redirectTo } = {} } = useIsLoggedIn();
|
const { isPending, isSuccess, data: { authenticated, redirectTo } = {} } = useIsLoggedIn();
|
||||||
const { data: profile, ...profileStatus } = useGetProfile();
|
//const { data: profile, ...profileStatus } = useGetProfile();
|
||||||
|
|
||||||
const { i18n } = useTranslation();
|
/*const { i18n } = useTranslation();
|
||||||
const [langCode, setLangCode] = useState(i18n.language);
|
const [langCode, setLangCode] = useState(i18n.language);
|
||||||
|
|
||||||
if (i18n.language !== langCode) {
|
if (i18n.language !== langCode) {
|
||||||
@ -23,10 +21,10 @@ export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
|
|||||||
if (profileStatus.isSuccess && profile && langCode !== profile.lang_code) {
|
if (profileStatus.isSuccess && profile && langCode !== profile.lang_code) {
|
||||||
setLangCode(profile.lang_code);
|
setLangCode(profile.lang_code);
|
||||||
}
|
}
|
||||||
}, [profile, profileStatus, i18n]);
|
}, [profile, profileStatus, i18n]);*/
|
||||||
|
|
||||||
if (isPending || profileStatus.isPending) {
|
if (isPending) {
|
||||||
return <LoadingOverlay />;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSuccess && !authenticated) {
|
if (isSuccess && !authenticated) {
|
||||||
@ -34,7 +32,7 @@ export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
|
|||||||
// trying to go to when they were redirected. This allows us to send them
|
// trying to go to when they were redirected. This allows us to send them
|
||||||
// along to that page after they login, which is a nicer user experience
|
// along to that page after they login, which is a nicer user experience
|
||||||
// than dropping them off on the home page.
|
// than dropping them off on the home page.
|
||||||
return <Navigate to={redirectTo ?? "/login"} state={{ from: location }} replace />;
|
return <Navigate to={redirectTo ?? "/login"} state={{ from: location }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children ?? null}</>;
|
return <>{children ?? null}</>;
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
/*@import url('https://fonts.googleapis.com/css2?family=Public Sans:wght@100;200;300;400;400;500;600;700;800;900;&family=Noto Serif:wght@100;200;300;400;400;500;600;700;800;900;1,500&display=swap');*/
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@ -50,63 +52,55 @@
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 240 10% 3.9%
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 240 10% 3.9%
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 240 10% 3.9%
|
||||||
--primary: 221.2 83.2% 53.3%;
|
--primary: 346.8 77.2% 49.8%
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 355.7 100% 97.3%
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 240 4.8% 95.9%
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 240 5.9% 10%
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 240 4.8% 95.9%
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--muted-foreground: 240 3.8% 46.1%
|
||||||
--accent: 210 40% 96.1%;
|
--accent: 240 4.8% 95.9%
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: 240 5.9% 10%
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 0% 98%
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 240 5.9% 90%
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 240 5.9% 90%
|
||||||
--ring: 221.2 83.2% 53.3%;
|
--ring: 346.8 77.2% 49.8%
|
||||||
--radius: 0.5rem;
|
--radius: 0.3rem;
|
||||||
--chart-1: 12 76% 61%;
|
|
||||||
--chart-2: 173 58% 39%;
|
|
||||||
--chart-3: 197 37% 24%;
|
|
||||||
--chart-4: 43 74% 66%;
|
|
||||||
--chart-5: 27 87% 67%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 20 14.3% 4.1%
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 0 0% 95%
|
||||||
--card: 222.2 84% 4.9%;
|
--card: 24 9.8% 10%
|
||||||
--card-foreground: 210 40% 98%;
|
--card-foreground: 0 0% 95%
|
||||||
--popover: 222.2 84% 4.9%;
|
--popover: 0 0% 9%
|
||||||
--popover-foreground: 210 40% 98%;
|
--popover-foreground: 0 0% 95%
|
||||||
--primary: 217.2 91.2% 59.8%;
|
--primary: 346.8 77.2% 49.8%
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--primary-foreground: 355.7 100% 97.3%
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--secondary: 240 3.7% 15.9%
|
||||||
--secondary-foreground: 210 40% 98%;
|
--secondary-foreground: 0 0% 98%
|
||||||
--muted: 217.2 32.6% 17.5%;
|
--muted: 0 0% 15%
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--muted-foreground: 240 5% 64.9%
|
||||||
--accent: 217.2 32.6% 17.5%;
|
--accent: 12 6.5% 15.1%
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: 0 0% 98%
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 85.7% 97.3%
|
||||||
--border: 217.2 32.6% 17.5%;
|
--border: 240 3.7% 15.9%
|
||||||
--input: 217.2 32.6% 17.5%;
|
--input: 240 3.7% 15.9%
|
||||||
--ring: 224.3 76.3% 48%;
|
--ring: 346.8 77.2% 49.8%
|
||||||
--chart-1: 220 70% 50%;
|
|
||||||
--chart-2: 160 60% 45%;
|
|
||||||
--chart-3: 30 80% 55%;
|
|
||||||
--chart-4: 280 65% 60%;
|
|
||||||
--chart-5: 340 75% 55%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
tab-size: 4;
|
tab-size: 4;
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
IRemoveOneDataProviderParams,
|
IRemoveOneDataProviderParams,
|
||||||
ISortItemDataProviderParam,
|
ISortItemDataProviderParam,
|
||||||
IUpdateOneDataProviderParams,
|
IUpdateOneDataProviderParams,
|
||||||
|
IUploadFileDataProviderParam,
|
||||||
} from "../hooks/useDataSource/DataSource";
|
} from "../hooks/useDataSource/DataSource";
|
||||||
import { createAxiosInstance } from "./axiosInstance";
|
import { createAxiosInstance } from "./axiosInstance";
|
||||||
|
|
||||||
@ -119,6 +120,23 @@ export const createAxiosDataProvider = (
|
|||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
uploadFile: async <R>(params: IUploadFileDataProviderParam): Promise<R> => {
|
||||||
|
const { path, file, key, onUploadProgress } = params;
|
||||||
|
const url = `${apiUrl}/${path}`;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append(key || "file", file);
|
||||||
|
|
||||||
|
const response = await httpClient.post<R>(url, formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
onUploadProgress,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
downloadPDF: async (
|
downloadPDF: async (
|
||||||
params: IDownloadPDFDataProviderParams
|
params: IDownloadPDFDataProviderParams
|
||||||
): Promise<IDownloadPDFDataProviderResponse> => {
|
): Promise<IDownloadPDFDataProviderResponse> => {
|
||||||
|
|||||||
@ -91,7 +91,7 @@ const onResponseError = (error: AxiosError): Promise<AxiosError> => {
|
|||||||
break;
|
break;
|
||||||
case 401:
|
case 401:
|
||||||
console.error("UnAuthorized");
|
console.error("UnAuthorized");
|
||||||
window.location.href = "/logout";
|
//window.location.href = "/logout";
|
||||||
break;
|
break;
|
||||||
case 403:
|
case 403:
|
||||||
console.error("Forbidden");
|
console.error("Forbidden");
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export const useLogin = (params?: UseMutationOptions<AuthActionResponse, Error,
|
|||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
const { success, redirectTo } = data;
|
const { success, redirectTo } = data;
|
||||||
if (success && redirectTo) {
|
if (success && redirectTo) {
|
||||||
navigate(redirectTo || "/quotes", { replace: false });
|
navigate(redirectTo || "/quotes");
|
||||||
}
|
}
|
||||||
if (onSuccess) {
|
if (onSuccess) {
|
||||||
onSuccess(data, variables, context);
|
onSuccess(data, variables, context);
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const useLogout = (params?: UseMutationOptions<AuthActionResponse, Error>
|
|||||||
|
|
||||||
const { success, redirectTo } = data;
|
const { success, redirectTo } = data;
|
||||||
if (success && redirectTo) {
|
if (success && redirectTo) {
|
||||||
navigate(redirectTo || "/", { replace: true });
|
navigate(redirectTo || "/");
|
||||||
}
|
}
|
||||||
if (onSuccess) {
|
if (onSuccess) {
|
||||||
onSuccess(data, variables, context);
|
onSuccess(data, variables, context);
|
||||||
|
|||||||
@ -64,6 +64,15 @@ export interface IDownloadPDFDataProviderResponse {
|
|||||||
filedata: Blob;
|
filedata: Blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUploadFileDataProviderParam {
|
||||||
|
path: string;
|
||||||
|
//resource: string;
|
||||||
|
//id: string;
|
||||||
|
file: File;
|
||||||
|
key: string;
|
||||||
|
onUploadProgress?: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICustomDataProviderParam {
|
export interface ICustomDataProviderParam {
|
||||||
url: string;
|
url: string;
|
||||||
method: "get" | "delete" | "head" | "options" | "post" | "put" | "patch";
|
method: "get" | "delete" | "head" | "options" | "post" | "put" | "patch";
|
||||||
@ -86,6 +95,7 @@ export interface IDataSource {
|
|||||||
downloadPDF: (
|
downloadPDF: (
|
||||||
params: IDownloadPDFDataProviderParams
|
params: IDownloadPDFDataProviderParams
|
||||||
) => Promise<IDownloadPDFDataProviderResponse>;
|
) => Promise<IDownloadPDFDataProviderResponse>;
|
||||||
|
uploadFile: <R>(params: IUploadFileDataProviderParam) => Promise<R>;
|
||||||
custom: <R>(params: ICustomDataProviderParam) => Promise<R>;
|
custom: <R>(params: ICustomDataProviderParam) => Promise<R>;
|
||||||
|
|
||||||
getApiUrl: () => string;
|
getApiUrl: () => string;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
type BaseKey = string | number;
|
type BaseKey = string | number;
|
||||||
|
|
||||||
type ParametrizedDataActions = "list" | "infinite";
|
type ParametrizedDataActions = "list" | "infinite";
|
||||||
type IdRequiredDataActions = "one" | "report";
|
type IdRequiredDataActions = "one" | "report" | "upload";
|
||||||
type IdsRequiredDataActions = "many";
|
type IdsRequiredDataActions = "many";
|
||||||
type DataMutationActions =
|
type DataMutationActions =
|
||||||
| "custom"
|
| "custom"
|
||||||
|
|||||||
@ -393,10 +393,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form_fields": {
|
"form_fields": {
|
||||||
"image": {
|
"logo": {
|
||||||
"label": "Logotipo",
|
"label": "Logotipo",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"desc": ""
|
"desc": "Este logotipo aparecerá en las propuestas exportadas en PDF",
|
||||||
|
"requirements": "Se permiten ficheros JPG o PNG. Las dimensiones del logotipo deben ser 200x100 píxeles o proporcionales.",
|
||||||
|
"upload": "Subir logotipo",
|
||||||
|
"invalid_format": "Sólo se permiten ficheros JPG o PNG",
|
||||||
|
"invalid_dimensions": "Las dimensiones del logotipo deben ser 200x100 píxeles o proporcionales."
|
||||||
},
|
},
|
||||||
"contact_information": {
|
"contact_information": {
|
||||||
"label": "Información de contacto",
|
"label": "Información de contacto",
|
||||||
|
|||||||
@ -14,7 +14,7 @@ i18n
|
|||||||
// init i18next
|
// init i18next
|
||||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||||
.init({
|
.init({
|
||||||
debug: false,
|
debug: true,
|
||||||
fallbackLng: "es",
|
fallbackLng: "es",
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
|
||||||
import defaultTheme from "tailwindcss/defaultTheme";
|
|
||||||
import plugin from "tailwindcss/plugin";
|
import plugin from "tailwindcss/plugin";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -18,69 +17,53 @@ export default {
|
|||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
// https://tailwindcss.com/docs/font-family#font-families
|
// https://tailwindcss.com/docs/font-family#font-families
|
||||||
fontFamily: {
|
/*fontFamily: {
|
||||||
sans: ['"Source Sans Pro"', ...defaultTheme.fontFamily.sans],
|
sans: ['"Source Sans Pro"', ...defaultTheme.fontFamily.sans],
|
||||||
|
},*/
|
||||||
|
fontFamily: {
|
||||||
|
display: "Public Sans, ui-sans-serif",
|
||||||
|
heading: "Noto Serif, ui-serif",
|
||||||
},
|
},
|
||||||
|
|
||||||
colors: {
|
colors: {
|
||||||
// https://adevade.github.io/color-scheme-generator/
|
border: "hsl(240, 5.9%, 90%)",
|
||||||
"brand-light": "#cdd6e7",
|
input: "hsl(240, 5.9%, 90%)",
|
||||||
brand: "#2a669f",
|
ring: "hsl(346.8, 77.2%, 49.8%)",
|
||||||
"brand-dark": "#1e344d",
|
background: "hsl(0, 0%, 100%)",
|
||||||
|
foreground: "hsl(240, 10%, 3.9%)",
|
||||||
// https://www.tailwindshades.com/#color=209%2C58%2C39.411764705882355&step-up=12&step-down=7&hue-shift=-59&name=great-blue&base-stop=5&v=1&overrides=e30%3D
|
|
||||||
denim: {
|
|
||||||
DEFAULT: "#2A669F",
|
|
||||||
50: "#E4F7F8",
|
|
||||||
100: "#CCEEF2",
|
|
||||||
200: "#9CD7E5",
|
|
||||||
300: "#6CB9D8",
|
|
||||||
400: "#3B94CB",
|
|
||||||
500: "#2A669F",
|
|
||||||
600: "#234B83",
|
|
||||||
700: "#1B3366",
|
|
||||||
800: "#14204A",
|
|
||||||
900: "#0C102E",
|
|
||||||
},
|
|
||||||
|
|
||||||
border: "hsl(var(--border))",
|
|
||||||
input: "hsl(var(--input))",
|
|
||||||
ring: "hsl(var(--ring))",
|
|
||||||
background: "hsl(var(--background))",
|
|
||||||
foreground: "hsl(var(--foreground))",
|
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: "hsl(var(--primary))",
|
DEFAULT: "hsl(346.8, 77.2%, 49.8%)",
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
foreground: "hsl(355.7, 100%, 97.3%)",
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: "hsl(var(--secondary))",
|
DEFAULT: "hsl(240, 4.8%, 95.9%)",
|
||||||
foreground: "hsl(var(--secondary-foreground))",
|
foreground: "hsl(240, 5.9%, 10%)",
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: "hsl(0, 84.2%, 60.2%)",
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
foreground: "hsl(0, 0%, 98%)",
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: "hsl(var(--muted))",
|
DEFAULT: "hsl(240, 4.8%, 95.9%)",
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
foreground: "hsl(240, 3.8%, 46.1%)",
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: "hsl(var(--accent))",
|
DEFAULT: "hsl(240, 4.8%, 95.9%)",
|
||||||
foreground: "hsl(var(--accent-foreground))",
|
foreground: "hsl(240, 5.9%, 10%)",
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
DEFAULT: "hsl(var(--popover))",
|
DEFAULT: "hsl(0, 0%, 100%)",
|
||||||
foreground: "hsl(var(--popover-foreground))",
|
foreground: "hsl(240, 10%, 3.9%)",
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: "hsl(var(--card))",
|
DEFAULT: "hsl(0, 0%, 100%)",
|
||||||
foreground: "hsl(var(--card-foreground))",
|
foreground: "hsl(240, 10%, 3.9%)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: "var(--radius)",
|
lg: "0.3rem",
|
||||||
md: "calc(var(--radius) - 2px)",
|
md: "calc(0.3rem - 2px)",
|
||||||
sm: "calc(var(--radius) - 4px)",
|
sm: "calc(0.3rem - 4px)",
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
"accordion-down": {
|
"accordion-down": {
|
||||||
|
|||||||
@ -59,6 +59,7 @@ services:
|
|||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
volumes:
|
volumes:
|
||||||
- backend_logs:/var/log
|
- backend_logs:/var/log
|
||||||
|
- backend_uploads:/api/uploads
|
||||||
ports:
|
ports:
|
||||||
- 3001:3001
|
- 3001:3001
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@ -27,7 +27,8 @@
|
|||||||
"@types/luxon": "^3.3.1",
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/module-alias": "^2.0.1",
|
"@types/module-alias": "^2.0.1",
|
||||||
"@types/morgan": "^1.9.4",
|
"@types/morgan": "^1.9.4",
|
||||||
"@types/node": "^20.12.11",
|
"@types/multer": "^1.4.12",
|
||||||
|
"@types/node": "^22.5.5",
|
||||||
"@types/passport": "^1.0.16",
|
"@types/passport": "^1.0.16",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/passport-local": "^1.0.38",
|
"@types/passport-local": "^1.0.38",
|
||||||
@ -72,8 +73,10 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.6.0",
|
"mysql2": "^3.6.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
|||||||
138833
server/public/css/tailwind.min.css
vendored
138833
server/public/css/tailwind.min.css
vendored
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
|||||||
<h1>public</h1>
|
|
||||||
138833
server/public/tailwind.min.css
vendored
138833
server/public/tailwind.min.css
vendored
File diff suppressed because it is too large
Load Diff
@ -36,6 +36,13 @@ module.exports = {
|
|||||||
cors_origin: "*",
|
cors_origin: "*",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
defaults: {
|
||||||
|
dealer_logos_upload_path:
|
||||||
|
"/home/rodax/Documentos/uecko-presupuestos/server/uploads/dealer-logos",
|
||||||
|
dealer_logo_placeholder:
|
||||||
|
"/home/rodax/Documentos/uecko-presupuestos/server/uploads/images/placeholder-200x100.png",
|
||||||
|
},
|
||||||
|
|
||||||
admin: {
|
admin: {
|
||||||
name: "Administrador",
|
name: "Administrador",
|
||||||
email: "darranz@rodax-software.com",
|
email: "darranz@rodax-software.com",
|
||||||
|
|||||||
@ -21,6 +21,11 @@ module.exports = {
|
|||||||
cors_origin: "https://presupuestos.uecko.com",
|
cors_origin: "https://presupuestos.uecko.com",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
defaults: {
|
||||||
|
dealer_logos_upload_path: "uploads/dealer-logos",
|
||||||
|
dealer_logo_placeholder: "uploads/images/logo-placeholder-200x100.png",
|
||||||
|
},
|
||||||
|
|
||||||
admin: {
|
admin: {
|
||||||
name: "Administrador",
|
name: "Administrador",
|
||||||
email: "darranz@rodax-software.com",
|
email: "darranz@rodax-software.com",
|
||||||
|
|||||||
@ -26,37 +26,3 @@ export class ContextFactory {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContextFactory.ts
|
|
||||||
/*export interface IContext {
|
|
||||||
adapter: ISequelizeAdapter;
|
|
||||||
repositoryManager: IRepositoryManager;
|
|
||||||
services: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ContextFactory {
|
|
||||||
private static instances: Map<string, ContextFactory<any>> = new Map();
|
|
||||||
|
|
||||||
public static getInstance(constructor: new () => T): ContextFactory {
|
|
||||||
const key = constructor.name;
|
|
||||||
if (!ContextFactory.instances.has(key)) {
|
|
||||||
ContextFactory.instances.set(key, new ContextFactory(constructor));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ContextFactory.instances.get(key)! as ContextFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private context: IContext;
|
|
||||||
|
|
||||||
private constructor(constructor: new () => T) {
|
|
||||||
this.context = {
|
|
||||||
adapter: createSequelizeAdapter(),
|
|
||||||
repositoryManager: RepositoryManager.getInstance(),
|
|
||||||
services: new constructor(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getContext(): IContext {
|
|
||||||
return this.context;
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|||||||
@ -57,6 +57,10 @@ export abstract class ExpressController implements IController {
|
|||||||
return this.res.status(httpStatus.NO_CONTENT).send();
|
return this.res.status(httpStatus.NO_CONTENT).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sendFile(filepath: string) {
|
||||||
|
return this.res.sendFile(filepath);
|
||||||
|
}
|
||||||
|
|
||||||
public downloadFile(filepath: string, filename: string, done?: any) {
|
public downloadFile(filepath: string, filename: string, done?: any) {
|
||||||
return this.res.download(filepath, filename, done);
|
return this.res.download(filepath, filename, done);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
IUseCase,
|
||||||
|
IUseCaseError,
|
||||||
|
IUseCaseRequest,
|
||||||
|
UseCaseError,
|
||||||
|
} from "@/contexts/common/application/useCases";
|
||||||
|
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||||
|
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||||
|
import { Result, UniqueID } from "@shared/contexts";
|
||||||
|
|
||||||
|
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||||
|
import { IProfileRepository, Profile } from "../domain";
|
||||||
|
|
||||||
|
export interface IGetProfileLogoUseCaseRequest extends IUseCaseRequest {
|
||||||
|
userId: UniqueID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetProfileLogoResponseOrError =
|
||||||
|
| Result<never, IUseCaseError> // Misc errors (value objects)
|
||||||
|
| Result<Profile, never>; // Success!
|
||||||
|
|
||||||
|
export class GetProfileLogoUseCase
|
||||||
|
implements IUseCase<IGetProfileLogoUseCaseRequest, Promise<GetProfileLogoResponseOrError>>
|
||||||
|
{
|
||||||
|
private _adapter: ISequelizeAdapter;
|
||||||
|
private _repositoryManager: IRepositoryManager;
|
||||||
|
|
||||||
|
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
|
||||||
|
this._adapter = props.adapter;
|
||||||
|
this._repositoryManager = props.repositoryManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRepositoryByName<T>(name: string) {
|
||||||
|
return this._repositoryManager.getRepository<T>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(request: IGetProfileLogoUseCaseRequest): Promise<GetProfileLogoResponseOrError> {
|
||||||
|
const { userId } = request;
|
||||||
|
|
||||||
|
// Validación de datos
|
||||||
|
// No hay en este caso
|
||||||
|
|
||||||
|
return await this._getProfileDealer(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getProfileDealer(userId: UniqueID) {
|
||||||
|
const transaction = this._adapter.startTransaction();
|
||||||
|
const dealerRepoBuilder = this.getRepositoryByName<IProfileRepository>("Profile");
|
||||||
|
|
||||||
|
let profile: Profile | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await transaction.complete(async (t) => {
|
||||||
|
const dealerRepo = dealerRepoBuilder({ transaction: t });
|
||||||
|
profile = await dealerRepo.getByUserId(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
return Result.fail(UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, "Profile not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok<Profile>(profile!);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const _error = error as IInfrastructureError;
|
||||||
|
console.error(error);
|
||||||
|
return Result.fail(
|
||||||
|
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Error al consultar el usuario", _error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
import {
|
||||||
|
IUseCase,
|
||||||
|
IUseCaseError,
|
||||||
|
IUseCaseRequest,
|
||||||
|
UseCaseError,
|
||||||
|
} from "@/contexts/common/application";
|
||||||
|
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||||
|
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||||
|
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||||
|
import { Result, UniqueID } from "@shared/contexts";
|
||||||
|
import { IProfileRepository, Profile } from "../domain";
|
||||||
|
|
||||||
|
export interface IUpdateProfileLogoUseCaseRequest extends IUseCaseRequest {
|
||||||
|
userId: UniqueID;
|
||||||
|
file: Express.Multer.File;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UploadProfileLogoResponseOrError =
|
||||||
|
| Result<never, IUseCaseError> // Misc errors (value objects)
|
||||||
|
| Result<Profile, never>; // Success!
|
||||||
|
|
||||||
|
export class UploadProfileLogoUseCase
|
||||||
|
implements IUseCase<IUpdateProfileLogoUseCaseRequest, Promise<UploadProfileLogoResponseOrError>>
|
||||||
|
{
|
||||||
|
private _adapter: ISequelizeAdapter;
|
||||||
|
private _repositoryManager: IRepositoryManager;
|
||||||
|
|
||||||
|
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
|
||||||
|
this._adapter = props.adapter;
|
||||||
|
this._repositoryManager = props.repositoryManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
request: IUpdateProfileLogoUseCaseRequest
|
||||||
|
): Promise<UploadProfileLogoResponseOrError> {
|
||||||
|
const { userId, file } = request;
|
||||||
|
|
||||||
|
// Comprobar que existe el profile
|
||||||
|
const exitsOrError = await this._getProfileDealer(userId);
|
||||||
|
if (exitsOrError.isFailure) {
|
||||||
|
const message = `Profile not found`;
|
||||||
|
return Result.fail(
|
||||||
|
UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, message, {
|
||||||
|
path: "userId",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = exitsOrError.object;
|
||||||
|
|
||||||
|
// Actualizar el perfil con datos actualizados
|
||||||
|
profile.logo = file.filename;
|
||||||
|
|
||||||
|
// Guardar los cambios
|
||||||
|
return this._saveProfile(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _saveProfile(updatedProfile: Profile) {
|
||||||
|
// Guardar el contacto
|
||||||
|
const transaction = this._adapter.startTransaction();
|
||||||
|
const profileRepository = this._getProfileRepository();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await transaction.complete(async (t) => {
|
||||||
|
const profileRepo = profileRepository({ transaction: t });
|
||||||
|
await profileRepo.update(updatedProfile);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Result.ok<Profile>(updatedProfile);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const _error = error as IInfrastructureError;
|
||||||
|
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getProfileDealer(userId: UniqueID) {
|
||||||
|
const transaction = this._adapter.startTransaction();
|
||||||
|
const dealerRepoBuilder = this._getProfileRepository();
|
||||||
|
|
||||||
|
let profile: Profile | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await transaction.complete(async (t) => {
|
||||||
|
const dealerRepo = dealerRepoBuilder({ transaction: t });
|
||||||
|
profile = await dealerRepo.getByUserId(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
return Result.fail(UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, "Profile not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok<Profile>(profile!);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const _error = error as IInfrastructureError;
|
||||||
|
return Result.fail(
|
||||||
|
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Error al consultar el usuario", _error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getProfileRepository() {
|
||||||
|
return this._repositoryManager.getRepository<IProfileRepository>("Profile");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +1,6 @@
|
|||||||
|
export * from "./GetProfile.useCase";
|
||||||
|
//export * from "./GetProfileByUserId.useCase";
|
||||||
|
export * from "./GetProfileLogo.useCase";
|
||||||
|
export * from "./profileServices";
|
||||||
export * from "./UpdateProfile.useCase";
|
export * from "./UpdateProfile.useCase";
|
||||||
|
export * from "./UploadProfileLogo.useCase";
|
||||||
|
|||||||
@ -23,6 +23,8 @@ export interface IProfileProps {
|
|||||||
defaultLegalTerms: Note;
|
defaultLegalTerms: Note;
|
||||||
defaultQuoteValidity: Note;
|
defaultQuoteValidity: Note;
|
||||||
defaultTax: Percentage;
|
defaultTax: Percentage;
|
||||||
|
|
||||||
|
logo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProfile {
|
export interface IProfile {
|
||||||
@ -39,6 +41,8 @@ export interface IProfile {
|
|||||||
|
|
||||||
defaultQuoteValidity: Note;
|
defaultQuoteValidity: Note;
|
||||||
defaultTax: Percentage;
|
defaultTax: Percentage;
|
||||||
|
|
||||||
|
logo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Profile extends AggregateRoot<IProfileProps> implements IProfile {
|
export class Profile extends AggregateRoot<IProfileProps> implements IProfile {
|
||||||
@ -110,4 +114,12 @@ export class Profile extends AggregateRoot<IProfileProps> implements IProfile {
|
|||||||
set defaultTax(newDefaultTax: Percentage) {
|
set defaultTax(newDefaultTax: Percentage) {
|
||||||
this.props.defaultTax = newDefaultTax;
|
this.props.defaultTax = newDefaultTax;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get logo(): string {
|
||||||
|
return this.props.logo;
|
||||||
|
}
|
||||||
|
|
||||||
|
set logo(newLogo: string) {
|
||||||
|
this.props.logo = newLogo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,12 @@ import {
|
|||||||
ISequelizeAdapter,
|
ISequelizeAdapter,
|
||||||
createSequelizeAdapter,
|
createSequelizeAdapter,
|
||||||
} from "@/contexts/common/infrastructure/sequelize";
|
} from "@/contexts/common/infrastructure/sequelize";
|
||||||
|
import { config } from "../../../config";
|
||||||
|
|
||||||
export interface IProfileContext {
|
export interface IProfileContext {
|
||||||
adapter: ISequelizeAdapter;
|
adapter: ISequelizeAdapter;
|
||||||
repositoryManager: IRepositoryManager;
|
repositoryManager: IRepositoryManager;
|
||||||
|
defaults: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProfileContext {
|
export class ProfileContext {
|
||||||
@ -17,6 +19,7 @@ export class ProfileContext {
|
|||||||
ProfileContext.instance = new ProfileContext({
|
ProfileContext.instance = new ProfileContext({
|
||||||
adapter: createSequelizeAdapter(),
|
adapter: createSequelizeAdapter(),
|
||||||
repositoryManager: RepositoryManager.getInstance(),
|
repositoryManager: RepositoryManager.getInstance(),
|
||||||
|
defaults: config.defaults,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -58,7 +58,9 @@ export class GetProfileController extends ExpressController {
|
|||||||
|
|
||||||
const profile = profileOrError.object;
|
const profile = profileOrError.object;
|
||||||
|
|
||||||
return this.ok<IGetProfileResponse_DTO>(this.presenter.map(profile, user, this.context));
|
return this.ok<IGetProfileResponse_DTO>(
|
||||||
|
await this.presenter.map(profile, user, this.context)
|
||||||
|
);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
return this.fail(e as IServerError);
|
return this.fail(e as IServerError);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,33 @@ import { AuthUser } from "@/contexts/auth/domain";
|
|||||||
import { Profile } from "@/contexts/profile/domain";
|
import { Profile } from "@/contexts/profile/domain";
|
||||||
import { IProfileContext } from "@/contexts/profile/infrastructure/Profile.context";
|
import { IProfileContext } from "@/contexts/profile/infrastructure/Profile.context";
|
||||||
import { IGetProfileResponse_DTO } from "@shared/contexts";
|
import { IGetProfileResponse_DTO } from "@shared/contexts";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import mime from "mime-types";
|
||||||
|
|
||||||
export interface IGetProfilePresenter {
|
export interface IGetProfilePresenter {
|
||||||
map: (profile: Profile, user: AuthUser, context: IProfileContext) => IGetProfileResponse_DTO;
|
map: (
|
||||||
|
profile: Profile,
|
||||||
|
user: AuthUser,
|
||||||
|
context: IProfileContext
|
||||||
|
) => Promise<IGetProfileResponse_DTO>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logoToBase64Image = async (imagePath: string) => {
|
||||||
|
const data = await fs.readFile(imagePath);
|
||||||
|
const mimeType = mime.lookup(imagePath);
|
||||||
|
return `data:${mimeType};base64,${data.toString("base64")}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const GetProfilePresenter: IGetProfilePresenter = {
|
export const GetProfilePresenter: IGetProfilePresenter = {
|
||||||
map: (profile: Profile, user: AuthUser, context: IProfileContext): IGetProfileResponse_DTO => {
|
map: async (
|
||||||
|
profile: Profile,
|
||||||
|
user: AuthUser,
|
||||||
|
context: IProfileContext
|
||||||
|
): Promise<IGetProfileResponse_DTO> => {
|
||||||
|
const profile_logo = profile.logo
|
||||||
|
? `${context.defaults.dealer_logos_upload_path}/${profile.logo}`
|
||||||
|
: context.defaults.dealer_logo_placeholder;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id.toString(),
|
id: user.id.toString(),
|
||||||
name: user.name.toString(),
|
name: user.name.toString(),
|
||||||
@ -28,6 +48,8 @@ export const GetProfilePresenter: IGetProfilePresenter = {
|
|||||||
default_legal_terms: profile.defaultLegalTerms.toString(),
|
default_legal_terms: profile.defaultLegalTerms.toString(),
|
||||||
default_quote_validity: profile.defaultQuoteValidity.toString(),
|
default_quote_validity: profile.defaultQuoteValidity.toString(),
|
||||||
default_tax: profile.defaultTax.convertScale(2).toObject(),
|
default_tax: profile.defaultTax.convertScale(2).toObject(),
|
||||||
|
|
||||||
|
logo: await logoToBase64Image(profile_logo),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -0,0 +1,100 @@
|
|||||||
|
import { AuthUser } from "@/contexts/auth/domain";
|
||||||
|
import { IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases";
|
||||||
|
import { IServerError } from "@/contexts/common/domain/errors";
|
||||||
|
import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure";
|
||||||
|
import { ExpressController } from "@/contexts/common/infrastructure/express";
|
||||||
|
import { GetProfileUseCase as GetProfileLogoUseCase } from "@/contexts/profile/application/GetProfile.useCase";
|
||||||
|
import { IProfileContext } from "../../../Profile.context";
|
||||||
|
import { IGetProfileLogoPresenter } from "./presenter";
|
||||||
|
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
interface AuthenticatedRequest extends Request {
|
||||||
|
user?: AuthUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetProfileLogoController extends ExpressController {
|
||||||
|
private useCase: GetProfileLogoUseCase;
|
||||||
|
private presenter: IGetProfileLogoPresenter;
|
||||||
|
private context: IProfileContext;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
props: {
|
||||||
|
useCase: GetProfileLogoUseCase;
|
||||||
|
presenter: IGetProfileLogoPresenter;
|
||||||
|
},
|
||||||
|
context: IProfileContext
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const { useCase, presenter } = props;
|
||||||
|
this.useCase = useCase;
|
||||||
|
this.presenter = presenter;
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeImpl(): Promise<any> {
|
||||||
|
const req = this.req as AuthenticatedRequest;
|
||||||
|
const user = <AuthUser>req.user;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
const errorMessage = "Unexpected missing user data";
|
||||||
|
const infraError = InfrastructureError.create(
|
||||||
|
InfrastructureError.UNEXCEPTED_ERROR,
|
||||||
|
errorMessage
|
||||||
|
);
|
||||||
|
return this.internalServerError(errorMessage, infraError);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const profileOrError = await this.useCase.execute({
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (profileOrError.isFailure) {
|
||||||
|
return this._handleExecuteError(profileOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = profileOrError.object;
|
||||||
|
const logo = this.presenter.map(profile, this.context);
|
||||||
|
console.log(logo);
|
||||||
|
return this.sendFile(logo);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return this.fail(e as IServerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleExecuteError(error: IUseCaseError) {
|
||||||
|
let errorMessage: string;
|
||||||
|
let infraError: IInfrastructureError;
|
||||||
|
|
||||||
|
switch (error.code) {
|
||||||
|
case UseCaseError.NOT_FOUND_ERROR:
|
||||||
|
errorMessage = "User has no associated profile";
|
||||||
|
|
||||||
|
infraError = InfrastructureError.create(
|
||||||
|
InfrastructureError.RESOURCE_NOT_READY,
|
||||||
|
errorMessage,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.notFoundError(errorMessage, infraError);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UseCaseError.UNEXCEPTED_ERROR:
|
||||||
|
errorMessage = error.message;
|
||||||
|
|
||||||
|
infraError = InfrastructureError.create(
|
||||||
|
InfrastructureError.UNEXCEPTED_ERROR,
|
||||||
|
errorMessage,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return this.internalServerError(errorMessage, infraError);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
errorMessage = error.message;
|
||||||
|
return this.clientError(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { GetProfileUseCase } from "@/contexts/profile/application/GetProfile.useCase";
|
||||||
|
import { IProfileContext } from "../../../Profile.context";
|
||||||
|
import { registerProfileRepository } from "../../../Profile.repository";
|
||||||
|
import { GetProfileLogoController } from "./GetProfileLogo.controller";
|
||||||
|
import { GetProfileLogoPresenter } from "./presenter";
|
||||||
|
|
||||||
|
export const createGetProfileLogoController = (context: IProfileContext) => {
|
||||||
|
registerProfileRepository(context);
|
||||||
|
|
||||||
|
return new GetProfileLogoController(
|
||||||
|
{
|
||||||
|
useCase: new GetProfileUseCase(context),
|
||||||
|
presenter: GetProfileLogoPresenter,
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { Profile } from "@/contexts/profile/domain";
|
||||||
|
import { IProfileContext } from "@/contexts/profile/infrastructure/Profile.context";
|
||||||
|
|
||||||
|
export interface IGetProfileLogoPresenter {
|
||||||
|
map: (profile: Profile, context: IProfileContext) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetProfileLogoPresenter: IGetProfileLogoPresenter = {
|
||||||
|
map: (profile: Profile, context: IProfileContext): string =>
|
||||||
|
profile.logo
|
||||||
|
? `${context.defaults.dealer_logos_upload_path}/${profile.logo}`
|
||||||
|
: context.defaults.dealer_logo_placeholder,
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./GetProfileLogo.presenter";
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./getProfile";
|
export * from "./getProfile";
|
||||||
export * from "./updateProfile";
|
export * from "./updateProfile";
|
||||||
|
export * from "./uploadProfileLogo";
|
||||||
|
|||||||
@ -0,0 +1,117 @@
|
|||||||
|
import { IUseCaseError, UseCaseError } from "@/contexts/common/application";
|
||||||
|
import { IServerError } from "@/contexts/common/domain/errors";
|
||||||
|
import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure";
|
||||||
|
import { ExpressController } from "@/contexts/common/infrastructure/express";
|
||||||
|
import { UploadProfileLogoUseCase } from "@/contexts/profile/application";
|
||||||
|
import { User } from "@/contexts/users/domain";
|
||||||
|
import { IProfileContext } from "../../../Profile.context";
|
||||||
|
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
interface AuthenticatedRequest extends Request {
|
||||||
|
user?: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UploadProfileLogoController extends ExpressController {
|
||||||
|
private useCase: UploadProfileLogoUseCase;
|
||||||
|
private context: IProfileContext;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
props: {
|
||||||
|
useCase: UploadProfileLogoUseCase;
|
||||||
|
},
|
||||||
|
context: IProfileContext
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const { useCase } = props;
|
||||||
|
this.useCase = useCase;
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeImpl() {
|
||||||
|
const req = this.req as AuthenticatedRequest;
|
||||||
|
const user = <User>req.user;
|
||||||
|
const file: Express.Multer.File = this.file;
|
||||||
|
|
||||||
|
if (!user || !file || !file.filename) {
|
||||||
|
const errorMessage = "Unexpected missing input data";
|
||||||
|
const infraError = InfrastructureError.create(
|
||||||
|
InfrastructureError.UNEXCEPTED_ERROR,
|
||||||
|
errorMessage
|
||||||
|
);
|
||||||
|
return this.internalServerError(errorMessage, infraError);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Llamar al caso de uso
|
||||||
|
const result = await this.useCase.execute({
|
||||||
|
userId: user.id,
|
||||||
|
file,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
return this._handleExecuteError(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.ok<void>();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return this.fail(e as IServerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleExecuteError(error: IUseCaseError) {
|
||||||
|
let errorMessage: string;
|
||||||
|
let infraError: IInfrastructureError;
|
||||||
|
|
||||||
|
switch (error.code) {
|
||||||
|
case UseCaseError.NOT_FOUND_ERROR:
|
||||||
|
errorMessage = "User has no associated profile";
|
||||||
|
|
||||||
|
infraError = InfrastructureError.create(
|
||||||
|
InfrastructureError.RESOURCE_NOT_FOUND_ERROR,
|
||||||
|
errorMessage,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.notFoundError(errorMessage, infraError);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UseCaseError.INVALID_INPUT_DATA:
|
||||||
|
errorMessage = "Profile data not valid";
|
||||||
|
|
||||||
|
infraError = InfrastructureError.create(
|
||||||
|
InfrastructureError.INVALID_INPUT_DATA,
|
||||||
|
"Datos a actualizar erróneos",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return this.invalidInputError(errorMessage, infraError);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UseCaseError.REPOSITORY_ERROR:
|
||||||
|
errorMessage = "Error updating profile";
|
||||||
|
infraError = InfrastructureError.create(
|
||||||
|
InfrastructureError.UNEXCEPTED_ERROR,
|
||||||
|
errorMessage,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return this.conflictError(errorMessage, infraError);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UseCaseError.UNEXCEPTED_ERROR:
|
||||||
|
errorMessage = error.message;
|
||||||
|
|
||||||
|
infraError = InfrastructureError.create(
|
||||||
|
InfrastructureError.UNEXCEPTED_ERROR,
|
||||||
|
errorMessage,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return this.internalServerError(errorMessage, infraError);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
errorMessage = error.message;
|
||||||
|
return this.clientError(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { UploadProfileLogoUseCase } from "@/contexts/profile/application";
|
||||||
|
import { IProfileContext } from "../../../Profile.context";
|
||||||
|
import { registerProfileRepository } from "../../../Profile.repository";
|
||||||
|
import { UploadProfileLogoController } from "./UploadProfileLogo.controller";
|
||||||
|
|
||||||
|
export const createUpdloadProfileLogoController = (context: IProfileContext) => {
|
||||||
|
registerProfileRepository(context);
|
||||||
|
|
||||||
|
return new UploadProfileLogoController(
|
||||||
|
{
|
||||||
|
useCase: new UploadProfileLogoUseCase(context),
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -32,6 +32,7 @@ class ProfileMapper
|
|||||||
const status = this.mapsValue(source, "status", DealerStatus.create);
|
const status = this.mapsValue(source, "status", DealerStatus.create);
|
||||||
const language = this.mapsValue(source, "lang_code", Language.createFromCode);
|
const language = this.mapsValue(source, "lang_code", Language.createFromCode);
|
||||||
const currency = this.mapsValue(source, "currency_code", CurrencyData.createFromCode);
|
const currency = this.mapsValue(source, "currency_code", CurrencyData.createFromCode);
|
||||||
|
const logo = source.logo;
|
||||||
|
|
||||||
const contactInformation = this.mapsValue(
|
const contactInformation = this.mapsValue(
|
||||||
source,
|
source,
|
||||||
@ -70,6 +71,8 @@ class ProfileMapper
|
|||||||
defaultLegalTerms,
|
defaultLegalTerms,
|
||||||
defaultQuoteValidity,
|
defaultQuoteValidity,
|
||||||
defaultTax,
|
defaultTax,
|
||||||
|
|
||||||
|
logo,
|
||||||
};
|
};
|
||||||
|
|
||||||
const id = this.mapsValue(source, "id", UniqueID.create);
|
const id = this.mapsValue(source, "id", UniqueID.create);
|
||||||
@ -92,6 +95,7 @@ class ProfileMapper
|
|||||||
default_legal_terms: source.defaultLegalTerms.toPrimitive(),
|
default_legal_terms: source.defaultLegalTerms.toPrimitive(),
|
||||||
default_quote_validity: source.defaultQuoteValidity.toPrimitive(),
|
default_quote_validity: source.defaultQuoteValidity.toPrimitive(),
|
||||||
default_tax: source.defaultTax.convertScale(2).toPrimitive(),
|
default_tax: source.defaultTax.convertScale(2).toPrimitive(),
|
||||||
|
logo: source.logo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,6 +57,7 @@ export class Dealer_Model extends Model<
|
|||||||
declare status: CreationOptional<string>;
|
declare status: CreationOptional<string>;
|
||||||
declare lang_code: CreationOptional<string>;
|
declare lang_code: CreationOptional<string>;
|
||||||
declare currency_code: CreationOptional<string>;
|
declare currency_code: CreationOptional<string>;
|
||||||
|
declare logo: CreationOptional<string>;
|
||||||
|
|
||||||
declare user: NonAttribute<User_Model>;
|
declare user: NonAttribute<User_Model>;
|
||||||
declare quotes: NonAttribute<Quote_Model>;
|
declare quotes: NonAttribute<Quote_Model>;
|
||||||
@ -92,6 +93,11 @@ export default (sequelize: Sequelize) => {
|
|||||||
defaultValue: 2100,
|
defaultValue: 2100,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
logo: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
|
||||||
lang_code: {
|
lang_code: {
|
||||||
type: DataTypes.STRING(2),
|
type: DataTypes.STRING(2),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
@ -2,12 +2,27 @@ import { checkUser } from "@/contexts/auth";
|
|||||||
import {
|
import {
|
||||||
createGetProfileController,
|
createGetProfileController,
|
||||||
createUpdateProfileController,
|
createUpdateProfileController,
|
||||||
|
ProfileContext,
|
||||||
} from "@/contexts/profile/infrastructure";
|
} from "@/contexts/profile/infrastructure";
|
||||||
|
import { createGetProfileLogoController } from "@/contexts/profile/infrastructure/express/controllers/getProfileLogo";
|
||||||
|
import { createUpdloadProfileLogoController } from "@/contexts/profile/infrastructure/express/controllers/uploadProfileLogo";
|
||||||
import { NextFunction, Request, Response, Router } from "express";
|
import { NextFunction, Request, Response, Router } from "express";
|
||||||
|
import { createMulterMiddleware } from "../upload.middleware";
|
||||||
|
|
||||||
|
const uploadProfileLogo = createMulterMiddleware({
|
||||||
|
uploadFolder: "uploads/dealer-logos",
|
||||||
|
mimeTypes: ["image/jpeg", "image/png"],
|
||||||
|
maxSize: 1024 * 1024 * 5, // 5Mb
|
||||||
|
});
|
||||||
|
|
||||||
export const profileRouter = (appRouter: Router) => {
|
export const profileRouter = (appRouter: Router) => {
|
||||||
const profileRoutes: Router = Router({ mergeParams: true });
|
const profileRoutes: Router = Router({ mergeParams: true });
|
||||||
|
|
||||||
|
profileRoutes.use((req: Request, res: Response, next: NextFunction) => {
|
||||||
|
res.locals["context"] = ProfileContext.getInstance();
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
profileRoutes.get("/", checkUser, (req: Request, res: Response, next: NextFunction) =>
|
profileRoutes.get("/", checkUser, (req: Request, res: Response, next: NextFunction) =>
|
||||||
createGetProfileController(res.locals["context"]).execute(req, res, next)
|
createGetProfileController(res.locals["context"]).execute(req, res, next)
|
||||||
);
|
);
|
||||||
@ -16,5 +31,17 @@ export const profileRouter = (appRouter: Router) => {
|
|||||||
createUpdateProfileController(res.locals["context"]).execute(req, res, next)
|
createUpdateProfileController(res.locals["context"]).execute(req, res, next)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
profileRoutes.get("/logo", checkUser, (req: Request, res: Response, next: NextFunction) =>
|
||||||
|
createGetProfileLogoController(res.locals["context"]).execute(req, res, next)
|
||||||
|
);
|
||||||
|
|
||||||
|
profileRoutes.post(
|
||||||
|
"/logo",
|
||||||
|
checkUser,
|
||||||
|
uploadProfileLogo.single("logo"),
|
||||||
|
(req: Request, res: Response, next: NextFunction) =>
|
||||||
|
createUpdloadProfileLogoController(res.locals["context"]).execute(req, res, next)
|
||||||
|
);
|
||||||
|
|
||||||
appRouter.use("/profile", profileRoutes);
|
appRouter.use("/profile", profileRoutes);
|
||||||
};
|
};
|
||||||
|
|||||||
55
server/src/infrastructure/express/api/upload.middleware.ts
Normal file
55
server/src/infrastructure/express/api/upload.middleware.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Request } from "express";
|
||||||
|
import fs from "fs";
|
||||||
|
import multer, { FileFilterCallback } from "multer";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
// Tipos personalizados para mejorar la legibilidad
|
||||||
|
type MimeType = string;
|
||||||
|
type MaxSize = number;
|
||||||
|
type UploadFolder = string;
|
||||||
|
|
||||||
|
type CreateUploadMiddlewareOptions = {
|
||||||
|
mimeTypes: MimeType[];
|
||||||
|
maxSize: MaxSize;
|
||||||
|
uploadFolder: UploadFolder;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ensureUploadFolderExists = (folderPath: string) => {
|
||||||
|
if (!fs.existsSync(folderPath)) {
|
||||||
|
fs.mkdirSync(folderPath, { recursive: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Función para configurar Multer
|
||||||
|
export function createMulterMiddleware(options: CreateUploadMiddlewareOptions) {
|
||||||
|
const { mimeTypes, maxSize, uploadFolder } = options;
|
||||||
|
|
||||||
|
ensureUploadFolderExists(uploadFolder);
|
||||||
|
|
||||||
|
// Configuración del almacenamiento3
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
cb(null, uploadFolder); // Carpeta donde se almacenan los archivos
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const ext = path.extname(file.originalname);
|
||||||
|
cb(null, `${file.originalname}-${Date.now()}.${ext}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filtro de archivos basado en mimetypes
|
||||||
|
const fileFilter = (req: Request, file: Express.Multer.File, cb: FileFilterCallback) => {
|
||||||
|
if (mimeTypes.includes(file.mimetype)) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error("Tipo de archivo no permitido"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Crear y devolver el middleware de Multer con la configuración personalizada
|
||||||
|
return multer({
|
||||||
|
storage: storage,
|
||||||
|
fileFilter: fileFilter,
|
||||||
|
limits: { fileSize: maxSize }, // Tamaño máximo del archivo
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import responseTime from "response-time";
|
|||||||
|
|
||||||
import { configurePassportAuth } from "@/contexts/auth";
|
import { configurePassportAuth } from "@/contexts/auth";
|
||||||
import morgan from "morgan";
|
import morgan from "morgan";
|
||||||
|
import multer from "multer";
|
||||||
import passport from "passport";
|
import passport from "passport";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { config } from "../../config";
|
import { config } from "../../config";
|
||||||
@ -72,6 +73,18 @@ app.set("port", process.env.PORT ?? 3001);
|
|||||||
// Public assets
|
// Public assets
|
||||||
app.use("/assets", express.static(path.join(__dirname, "/public")));
|
app.use("/assets", express.static(path.join(__dirname, "/public")));
|
||||||
|
|
||||||
|
// Manejo de errores de multer
|
||||||
|
app.use((err: any, req: any, res: any, next: any) => {
|
||||||
|
if (err instanceof multer.MulterError) {
|
||||||
|
// Error de multer
|
||||||
|
return res.status(400).json({ error: err.message });
|
||||||
|
} else if (err) {
|
||||||
|
// Otro tipo de error
|
||||||
|
return res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// API
|
// API
|
||||||
app.use("/api/v1", v1Routes());
|
app.use("/api/v1", v1Routes());
|
||||||
|
|
||||||
|
|||||||
BIN
server/uploads/images/placeholder-200x100.png
Normal file
BIN
server/uploads/images/placeholder-200x100.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
@ -1012,13 +1012,27 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/express" "*"
|
"@types/express" "*"
|
||||||
|
|
||||||
"@types/node@*", "@types/node@^20.12.11":
|
"@types/multer@^1.4.12":
|
||||||
|
version "1.4.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.12.tgz#da67bd0c809f3a63fe097c458c0d4af1fea50ab7"
|
||||||
|
integrity sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==
|
||||||
|
dependencies:
|
||||||
|
"@types/express" "*"
|
||||||
|
|
||||||
|
"@types/node@*":
|
||||||
version "20.14.10"
|
version "20.14.10"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.10.tgz#a1a218290f1b6428682e3af044785e5874db469a"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.10.tgz#a1a218290f1b6428682e3af044785e5874db469a"
|
||||||
integrity sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==
|
integrity sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~5.26.4"
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
|
"@types/node@^22.5.5":
|
||||||
|
version "22.5.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.5.tgz#52f939dd0f65fc552a4ad0b392f3c466cc5d7a44"
|
||||||
|
integrity sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~6.19.2"
|
||||||
|
|
||||||
"@types/passport-jwt@^4.0.1":
|
"@types/passport-jwt@^4.0.1":
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/passport-jwt/-/passport-jwt-4.0.1.tgz#080fbe934fb9f6954fb88ec4cdf4bb2cc7c4d435"
|
resolved "https://registry.yarnpkg.com/@types/passport-jwt/-/passport-jwt-4.0.1.tgz#080fbe934fb9f6954fb88ec4cdf4bb2cc7c4d435"
|
||||||
@ -4375,7 +4389,7 @@ mime-db@1.52.0:
|
|||||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||||
|
|
||||||
mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34:
|
mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34:
|
||||||
version "2.1.35"
|
version "2.1.35"
|
||||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||||
@ -6047,6 +6061,11 @@ undici-types@~5.26.4:
|
|||||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||||
|
|
||||||
|
undici-types@~6.19.2:
|
||||||
|
version "6.19.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
|
||||||
|
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
|
||||||
|
|
||||||
universalify@^2.0.0:
|
universalify@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
|
||||||
|
|||||||
@ -18,5 +18,6 @@ export interface IGetProfileResponse_DTO {
|
|||||||
status: string;
|
status: string;
|
||||||
lang_code: string;
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
logo: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export interface IUpdateProfile_Request_DTO {
|
|||||||
default_legal_terms: string;
|
default_legal_terms: string;
|
||||||
default_quote_validity: string;
|
default_quote_validity: string;
|
||||||
default_tax: IPercentage_DTO;
|
default_tax: IPercentage_DTO;
|
||||||
|
logo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureUpdateProfile_Request_DTOIsValid(userDTO: IUpdateProfile_Request_DTO) {
|
export function ensureUpdateProfile_Request_DTOIsValid(userDTO: IUpdateProfile_Request_DTO) {
|
||||||
@ -21,6 +22,7 @@ export function ensureUpdateProfile_Request_DTOIsValid(userDTO: IUpdateProfile_R
|
|||||||
amount: Joi.number().allow(null),
|
amount: Joi.number().allow(null),
|
||||||
scale: Joi.number(),
|
scale: Joi.number(),
|
||||||
}).optional(),
|
}).optional(),
|
||||||
|
logo: Joi.string().optional().allow(null).allow("").default(""),
|
||||||
}).unknown(true);
|
}).unknown(true);
|
||||||
|
|
||||||
const result = RuleValidator.validate<IUpdateProfile_Request_DTO>(schema, userDTO);
|
const result = RuleValidator.validate<IUpdateProfile_Request_DTO>(schema, userDTO);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user