.
This commit is contained in:
parent
d5281fb1f5
commit
0b00b84289
@ -1,2 +1,2 @@
|
||||
VITE_API_URL=http://127.0.0.1:4001/api/v1
|
||||
VITE_API_URL=http://192.168.0.111:4001/api/v1
|
||||
VITE_API_KEY=e175f809ba71fb2765ad5e60f9d77596-es19
|
||||
@ -5,12 +5,12 @@ import {
|
||||
LoginPage,
|
||||
LogoutPage,
|
||||
QuoteCreate,
|
||||
QuoteEdit,
|
||||
SettingsEditor,
|
||||
SettingsLayout,
|
||||
StartPage,
|
||||
} from "./app";
|
||||
import { CatalogLayout, CatalogList } from "./app/catalog";
|
||||
import { DashboardPage } from "./app/dashboard";
|
||||
import { QuotesLayout } from "./app/quotes/layout";
|
||||
import { QuotesList } from "./app/quotes/list";
|
||||
import { ProtectedRoute } from "./components";
|
||||
@ -26,14 +26,6 @@ export const Routes = () => {
|
||||
|
||||
// Define routes accessible only to authenticated users
|
||||
const routesForAuthenticatedOnly = [
|
||||
{
|
||||
path: "/home",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<DashboardPage />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/catalog",
|
||||
element: (
|
||||
@ -84,6 +76,10 @@ export const Routes = () => {
|
||||
path: "add",
|
||||
element: <QuoteCreate />,
|
||||
},
|
||||
{
|
||||
path: "edit",
|
||||
element: <QuoteEdit />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -32,7 +32,7 @@ export const ErrorPage = (props: ErrorPageProps) => {
|
||||
<Button id='backButton' onClick={() => navigate(-1)}>
|
||||
Return to Previous Page
|
||||
</Button>
|
||||
<Button id='homeButton' onClick={() => navigate("/home")}>
|
||||
<Button id='homeButton' onClick={() => navigate("/")}>
|
||||
Return to Home Page
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@ -21,5 +21,5 @@ export const StartPage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return <Navigate to={"/home"} replace />;
|
||||
return <Navigate to={"/quotes"} replace />;
|
||||
};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Layout, LayoutContent, LayoutHeader } from "@/components";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { CatalogProvider } from "./CatalogContext";
|
||||
|
||||
export const CatalogLayout = ({ children }: PropsWithChildren) => {
|
||||
@ -8,14 +7,7 @@ export const CatalogLayout = ({ children }: PropsWithChildren) => {
|
||||
<CatalogProvider>
|
||||
<Layout>
|
||||
<LayoutHeader />
|
||||
<LayoutContent>
|
||||
<div className='flex items-center'>
|
||||
<h1 className='text-lg font-semibold md:text-2xl'>
|
||||
<Trans i18nKey='catalog.title' />
|
||||
</h1>
|
||||
</div>
|
||||
{children}
|
||||
</LayoutContent>
|
||||
<LayoutContent>{children}</LayoutContent>
|
||||
</Layout>
|
||||
</CatalogProvider>
|
||||
);
|
||||
|
||||
@ -1,9 +1,19 @@
|
||||
import { DataTableProvider } from "@/lib/hooks";
|
||||
import { Trans } from "react-i18next";
|
||||
import { CatalogDataTable } from "./components";
|
||||
|
||||
export const CatalogList = () => {
|
||||
return (
|
||||
<DataTableProvider>
|
||||
<div className='flex items-center justify-between space-y-2'>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold tracking-tight'>
|
||||
<Trans i18nKey='catalog.list.title' />
|
||||
</h2>
|
||||
<p className='text-muted-foreground'>descripción</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CatalogDataTable />
|
||||
</DataTableProvider>
|
||||
);
|
||||
|
||||
@ -9,7 +9,6 @@ import {
|
||||
} from "@/components";
|
||||
import { Input } from "@/ui";
|
||||
import { t } from "i18next";
|
||||
import { HashIcon } from "lucide-react";
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { useDetailColumns } from "../../hooks";
|
||||
import { SortableDataTable } from "../SortableDataTable";
|
||||
@ -24,36 +23,22 @@ export const QuoteDetailsCardEditor = () => {
|
||||
|
||||
const columns = useDetailColumns(
|
||||
[
|
||||
{
|
||||
/*{
|
||||
id: "row_id" as const,
|
||||
header: () => (
|
||||
<HashIcon aria-label='Orden de fila' className='items-center justify-center w-4 h-4' />
|
||||
),
|
||||
accessorFn: (originalRow: unknown, index: number) => index + 1,
|
||||
size: 26,
|
||||
size: 5,
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
},
|
||||
{
|
||||
id: "description" as const,
|
||||
accessorKey: "description",
|
||||
size: 400,
|
||||
cell: ({ row: { index }, column: { id } }) => {
|
||||
return (
|
||||
<FormTextAreaField
|
||||
autoSize
|
||||
control={control}
|
||||
{...register(`items.${index}.description`)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
},*/
|
||||
{
|
||||
id: "quantity" as const,
|
||||
accessorKey: "quantity",
|
||||
header: "quantity",
|
||||
size: 60,
|
||||
size: 5,
|
||||
cell: ({ row: { index }, column: { id } }) => {
|
||||
return (
|
||||
<FormTextField
|
||||
@ -65,37 +50,55 @@ export const QuoteDetailsCardEditor = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "unit_measure" as const,
|
||||
accessorKey: "unit_measure",
|
||||
header: "unit_measure",
|
||||
size: 60,
|
||||
id: "description" as const,
|
||||
accessorKey: "description",
|
||||
cell: ({ row: { index }, column: { id } }) => {
|
||||
return <Input key={id} {...register(`items.${index}.unit_measure`)} />;
|
||||
return (
|
||||
<FormTextAreaField
|
||||
autoSize
|
||||
control={control}
|
||||
{...register(`items.${index}.description`)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: "retail_price" as const,
|
||||
accessorKey: "retail_price",
|
||||
header: "retail_price",
|
||||
size: 10,
|
||||
cell: ({ row: { index }, column: { id } }) => {
|
||||
return <Input key={id} {...register(`items.${index}.retail_price`)} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "unit_price" as const,
|
||||
accessorKey: "unit_price",
|
||||
header: "unit_price",
|
||||
id: "price" as const,
|
||||
accessorKey: "price",
|
||||
header: "price",
|
||||
size: 10,
|
||||
cell: ({ row: { index }, column: { id } }) => {
|
||||
return <FormMoneyField control={control} {...register(`items.${index}.unit_price`)} />;
|
||||
return <FormMoneyField control={control} {...register(`items.${index}.price`)} />;
|
||||
},
|
||||
} /*
|
||||
{
|
||||
id: "subtotal" as const,
|
||||
accessorKey: "subtotal",
|
||||
header: "subtotal",
|
||||
},
|
||||
{
|
||||
id: "tax_amount" as const,
|
||||
accessorKey: "tax_amount",
|
||||
header: "tax_amount",
|
||||
id: "discount" as const,
|
||||
accessorKey: "discount",
|
||||
header: "discount",
|
||||
size: 5,
|
||||
cell: ({ row: { index }, column: { id } }) => {
|
||||
return <FormMoneyField control={control} {...register(`items.${index}.discount`)} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "total" as const,
|
||||
accessorKey: "total",
|
||||
header: "total",
|
||||
},*/,
|
||||
size: 10,
|
||||
cell: ({ row: { index }, column: { id } }) => {
|
||||
return <FormMoneyField control={control} {...register(`items.${index}.total`)} />;
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
enableDragHandleColumn: true,
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { CancelButton, FormDatePickerField, FormTextAreaField, FormTextField } from "@/components";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
|
||||
import { SubmitButton } from "@/components";
|
||||
import { useGetIdentity } from "@/lib/hooks";
|
||||
import { Badge, Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui";
|
||||
import { t } from "i18next";
|
||||
import { useState } from "react";
|
||||
import { Button, Form } from "@/ui";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import { QuoteGeneralCardEditor } from "./components/editors";
|
||||
import { useQuotes } from "./hooks";
|
||||
|
||||
type QuoteDataForm = {
|
||||
@ -23,45 +22,35 @@ type QuoteDataForm = {
|
||||
items: any[];
|
||||
};
|
||||
|
||||
type QuoteCreateProps = {
|
||||
/*type QuoteCreateProps = {
|
||||
isOverModal?: boolean;
|
||||
};
|
||||
};*/
|
||||
|
||||
export const QuoteCreate = ({ isOverModal }: QuoteCreateProps) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
export const QuoteCreate = () => {
|
||||
//const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data: userIdentity } = useGetIdentity();
|
||||
console.log(userIdentity);
|
||||
//const { data: userIdentity } = useGetIdentity();
|
||||
//console.log(userIdentity);
|
||||
|
||||
const { useQuery, useMutation } = useQuotes();
|
||||
|
||||
const { data } = useQuery;
|
||||
const { useMutation } = useQuotes();
|
||||
const { mutate } = useMutation;
|
||||
|
||||
const form = useForm<QuoteDataForm>({
|
||||
mode: "onBlur",
|
||||
values: data,
|
||||
defaultValues: {
|
||||
date: "",
|
||||
reference: "",
|
||||
date: Date.now().toLocaleString(),
|
||||
customer_information: "",
|
||||
lang_code: "",
|
||||
currency_code: "",
|
||||
payment_method: "",
|
||||
notes: "",
|
||||
validity: "",
|
||||
items: [],
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<QuoteDataForm> = async (data) => {
|
||||
const onSubmit: SubmitHandler<QuoteDataForm> = async (formData) => {
|
||||
alert(JSON.stringify(formData));
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
data.currency_code = "EUR";
|
||||
data.lang_code = String(userIdentity?.language);
|
||||
mutate(data);
|
||||
//setLoading(true);
|
||||
mutate(formData);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
//setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -77,37 +66,53 @@ export const QuoteCreate = ({ isOverModal }: QuoteCreateProps) => {
|
||||
<h1 className='flex-1 text-xl font-semibold tracking-tight shrink-0 whitespace-nowrap sm:grow-0'>
|
||||
{t("quotes.create.title")}
|
||||
</h1>
|
||||
<Badge variant='default' className='ml-auto sm:ml-0'>
|
||||
{t("quotes.status.draft")}
|
||||
</Badge>
|
||||
<div className='items-center hidden gap-2 md:ml-auto md:flex'>
|
||||
<Button variant='outline' size='sm'>
|
||||
{t("quotes.create.buttons.discard")}
|
||||
</Button>
|
||||
<SubmitButton variant={form.formState.isDirty ? "default" : "outline"} size='sm'>
|
||||
{t("quotes.create.buttons.save_quote")}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
|
||||
<div className='grid max-w-lg gap-6'>
|
||||
<FormTextField
|
||||
label={t("quotes.create.form_fields.reference.label")}
|
||||
description={t("quotes.create.form_fields.reference.desc")}
|
||||
disabled={form.formState.disabled}
|
||||
placeholder={t("quotes.create.form_fields.reference.placeholder")}
|
||||
{...form.register("reference", {
|
||||
required: false,
|
||||
})}
|
||||
/>
|
||||
<FormDatePickerField
|
||||
required
|
||||
label={t("quotes.create.form_fields.date.label")}
|
||||
description={t("quotes.create.form_fields.date.desc")}
|
||||
disabled={form.formState.disabled}
|
||||
placeholder={t("quotes.create.form_fields.date.placeholder")}
|
||||
{...form.register("date", {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
|
||||
<div className='grid grid-cols-1 grid-rows-2 gap-6'>
|
||||
<FormTextAreaField
|
||||
className='row-span-2'
|
||||
required
|
||||
label={t("quotes.create.form_fields.customer_information.label")}
|
||||
description={t("quotes.create.form_fields.customer_information.desc")}
|
||||
disabled={form.formState.disabled}
|
||||
placeholder={t("quotes.create.form_fields.customer_information.placeholder")}
|
||||
{...form.register("customer_information", {
|
||||
required: true,
|
||||
})}
|
||||
errors={form.formState.errors}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs defaultValue='general' className='space-y-4'>
|
||||
<TabsList>
|
||||
<TabsTrigger value='general'>{t("quotes.create.tabs.general")}</TabsTrigger>
|
||||
<TabsTrigger value='items'>{t("quotes.create.tabs.items")}</TabsTrigger>
|
||||
<TabsTrigger value='documents'>{t("quotes.create.tabs.documents")}</TabsTrigger>
|
||||
<TabsTrigger value='history'>{t("quotes.create.tabs.history")}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='general'>
|
||||
<QuoteGeneralCardEditor />
|
||||
</TabsContent>
|
||||
<TabsContent value='items'></TabsContent>
|
||||
<TabsContent value='documents'></TabsContent>
|
||||
<TabsContent value='history'></TabsContent>
|
||||
</Tabs>
|
||||
<div className='flex items-center justify-center gap-2 md:hidden'>
|
||||
<Button variant='outline' size='sm'>
|
||||
{t("quotes.create.buttons.discard")}
|
||||
</Button>
|
||||
<Button size='sm'>{t("quotes.create.buttons.save_quote")}</Button>
|
||||
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<CancelButton
|
||||
variant='outline'
|
||||
size='sm'
|
||||
label={t("quotes.create.buttons.discard")}
|
||||
></CancelButton>
|
||||
|
||||
<SubmitButton size='sm' label={t("common.continue")}></SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
128
client/src/app/quotes/edit.tsx
Normal file
128
client/src/app/quotes/edit.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
|
||||
import { SubmitButton } from "@/components";
|
||||
import { useGetIdentity } from "@/lib/hooks";
|
||||
import { Badge, Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui";
|
||||
import { t } from "i18next";
|
||||
import { useState } from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import {
|
||||
QuoteDetailsCardEditor,
|
||||
QuoteDocumentsCardEditor,
|
||||
QuoteGeneralCardEditor,
|
||||
} from "./components/editors";
|
||||
import { useQuotes } from "./hooks";
|
||||
|
||||
type QuoteDataForm = {
|
||||
id: string;
|
||||
status: string;
|
||||
date: string;
|
||||
reference: string;
|
||||
customer_information: string;
|
||||
lang_code: string;
|
||||
currency_code: string;
|
||||
payment_method: string;
|
||||
notes: string;
|
||||
validity: string;
|
||||
items: any[];
|
||||
};
|
||||
|
||||
type QuoteCreateProps = {
|
||||
isOverModal?: boolean;
|
||||
};
|
||||
|
||||
export const QuoteEdit = ({ isOverModal }: QuoteCreateProps) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data: userIdentity } = useGetIdentity();
|
||||
console.log(userIdentity);
|
||||
|
||||
const { useQuery, useMutation } = useQuotes();
|
||||
|
||||
const { data } = useQuery;
|
||||
const { mutate } = useMutation;
|
||||
|
||||
const form = useForm<QuoteDataForm>({
|
||||
mode: "onBlur",
|
||||
values: data,
|
||||
defaultValues: {
|
||||
date: "",
|
||||
reference: "",
|
||||
customer_information: "",
|
||||
lang_code: "",
|
||||
currency_code: "",
|
||||
payment_method: "",
|
||||
notes: "",
|
||||
validity: "",
|
||||
items: [],
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<QuoteDataForm> = async (data) => {
|
||||
alert(JSON.stringify(data));
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
data.currency_code = "EUR";
|
||||
data.lang_code = String(userIdentity?.language);
|
||||
|
||||
mutate(data);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Button variant='outline' size='icon' className='h-7 w-7'>
|
||||
<ChevronLeft className='w-4 h-4' />
|
||||
<span className='sr-only'>{t("quotes.common.back")}</span>
|
||||
</Button>
|
||||
<h1 className='flex-1 text-xl font-semibold tracking-tight shrink-0 whitespace-nowrap sm:grow-0'>
|
||||
{t("quotes.create.title")}
|
||||
</h1>
|
||||
<Badge variant='default' className='ml-auto sm:ml-0'>
|
||||
{t("quotes.status.draft")}
|
||||
</Badge>
|
||||
<div className='items-center hidden gap-2 md:ml-auto md:flex'>
|
||||
<Button variant='outline' size='sm'>
|
||||
{t("quotes.create.buttons.discard")}
|
||||
</Button>
|
||||
<SubmitButton variant={form.formState.isDirty ? "default" : "outline"} size='sm'>
|
||||
{t("quotes.create.buttons.save_quote")}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs defaultValue='general' className='space-y-4'>
|
||||
<TabsList>
|
||||
<TabsTrigger value='general'>{t("quotes.create.tabs.general")}</TabsTrigger>
|
||||
<TabsTrigger value='items'>{t("quotes.create.tabs.items")}</TabsTrigger>
|
||||
<TabsTrigger value='documents'>{t("quotes.create.tabs.documents")}</TabsTrigger>
|
||||
<TabsTrigger value='history'>{t("quotes.create.tabs.history")}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='general'>
|
||||
<QuoteGeneralCardEditor />
|
||||
</TabsContent>
|
||||
<TabsContent value='items'>
|
||||
<QuoteDetailsCardEditor />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='documents'>
|
||||
<QuoteDocumentsCardEditor />
|
||||
</TabsContent>
|
||||
<TabsContent value='history'></TabsContent>
|
||||
</Tabs>
|
||||
<div className='flex items-center justify-center gap-2 md:hidden'>
|
||||
<Button variant='outline' size='sm'>
|
||||
{t("quotes.create.buttons.discard")}
|
||||
</Button>
|
||||
<Button size='sm'>{t("quotes.create.buttons.save_quote")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@ -37,10 +37,12 @@ export const useQuotes = (params?: UseQuotesGetParamsType) => {
|
||||
id = UniqueID.generateNewID().object.toString();
|
||||
}
|
||||
|
||||
return dataSource.updateOne({
|
||||
return dataSource.createOne({
|
||||
resource: "quotes",
|
||||
data,
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
id,
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./create";
|
||||
export * from "./edit";
|
||||
export * from "./list";
|
||||
|
||||
@ -1,15 +1,443 @@
|
||||
import { DataTableProvider } from "@/lib/hooks";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
CreditCard,
|
||||
File,
|
||||
ListFilter,
|
||||
MoreVertical,
|
||||
Truck,
|
||||
} from "lucide-react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { QuotesDataTable } from "./components";
|
||||
|
||||
export const QuotesList = () => (
|
||||
<DataTableProvider>
|
||||
<div className='flex items-center'>
|
||||
<h1 className='text-lg font-semibold md:text-2xl'>
|
||||
<Trans i18nKey='quotes.title' />
|
||||
</h1>
|
||||
</div>
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
Progress,
|
||||
Separator,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/ui";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
<QuotesDataTable />
|
||||
</DataTableProvider>
|
||||
);
|
||||
export const QuotesList = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<DataTableProvider>
|
||||
<div className='flex items-center justify-between space-y-2'>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold tracking-tight'>
|
||||
<Trans i18nKey='quotes.list.title' />
|
||||
</h2>
|
||||
<p className='text-muted-foreground'>descripción</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<QuotesDataTable />
|
||||
|
||||
<div className='grid items-start flex-1 gap-4 p-4 sm:px-6 sm:py-0 md:gap-8 lg:grid-cols-3 xl:grid-cols-3'>
|
||||
<div className='grid items-start gap-4 auto-rows-max md:gap-8 lg:col-span-2'>
|
||||
<div className='grid gap-4 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-2 xl:grid-cols-4'>
|
||||
<Card className='sm:col-span-2' x-chunk='dashboard-05-chunk-0'>
|
||||
<CardHeader className='pb-3'>
|
||||
<CardTitle>Tus Cotizaciones</CardTitle>
|
||||
<CardDescription className='max-w-lg leading-relaxed text-balance'>
|
||||
Introducing Our Dynamic Orders Dashboard for Seamless Management and Insightful
|
||||
Analysis.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Button onClick={() => navigate("add", { relative: "path" })}>
|
||||
Crear nueva cotización
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card x-chunk='dashboard-05-chunk-1'>
|
||||
<CardHeader className='pb-2'>
|
||||
<CardDescription>This Week</CardDescription>
|
||||
<CardTitle className='text-4xl'>$1,329</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-xs text-muted-foreground'>+25% from last week</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Progress value={25} aria-label='25% increase' />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card x-chunk='dashboard-05-chunk-2'>
|
||||
<CardHeader className='pb-2'>
|
||||
<CardDescription>This Month</CardDescription>
|
||||
<CardTitle className='text-4xl'>$5,329</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-xs text-muted-foreground'>+10% from last month</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Progress value={12} aria-label='12% increase' />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
<Tabs defaultValue='week'>
|
||||
<div className='flex items-center'>
|
||||
<TabsList>
|
||||
<TabsTrigger value='week'>Week</TabsTrigger>
|
||||
<TabsTrigger value='month'>Month</TabsTrigger>
|
||||
<TabsTrigger value='year'>Year</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className='flex items-center gap-2 ml-auto'>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='outline' size='sm' className='gap-1 text-sm h-7'>
|
||||
<ListFilter className='h-3.5 w-3.5' />
|
||||
<span className='sr-only sm:not-sr-only'>Filter</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuCheckboxItem checked>Fulfilled</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem>Declined</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem>Refunded</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button size='sm' variant='outline' className='gap-1 text-sm h-7'>
|
||||
<File className='h-3.5 w-3.5' />
|
||||
<span className='sr-only sm:not-sr-only'>Export</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent value='week'>
|
||||
<Card x-chunk='dashboard-05-chunk-3'>
|
||||
<CardHeader className='px-7'>
|
||||
<CardTitle>Orders</CardTitle>
|
||||
<CardDescription>Recent orders from your store.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Customer</TableHead>
|
||||
<TableHead className='hidden sm:table-cell'>Type</TableHead>
|
||||
<TableHead className='hidden sm:table-cell'>Status</TableHead>
|
||||
<TableHead className='hidden md:table-cell'>Date</TableHead>
|
||||
<TableHead className='text-right'>Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow className='bg-accent'>
|
||||
<TableCell>
|
||||
<div className='font-medium'>Liam Johnson</div>
|
||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||
liam@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>
|
||||
<Badge className='text-xs' variant='secondary'>
|
||||
Fulfilled
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
|
||||
<TableCell className='text-right'>$250.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className='font-medium'>Olivia Smith</div>
|
||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||
olivia@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>Refund</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>
|
||||
<Badge className='text-xs' variant='outline'>
|
||||
Declined
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className='hidden md:table-cell'>2023-06-24</TableCell>
|
||||
<TableCell className='text-right'>$150.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className='font-medium'>Noah Williams</div>
|
||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||
noah@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>Subscription</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>
|
||||
<Badge className='text-xs' variant='secondary'>
|
||||
Fulfilled
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className='hidden md:table-cell'>2023-06-25</TableCell>
|
||||
<TableCell className='text-right'>$350.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className='font-medium'>Emma Brown</div>
|
||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||
emma@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>
|
||||
<Badge className='text-xs' variant='secondary'>
|
||||
Fulfilled
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className='hidden md:table-cell'>2023-06-26</TableCell>
|
||||
<TableCell className='text-right'>$450.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className='font-medium'>Liam Johnson</div>
|
||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||
liam@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>
|
||||
<Badge className='text-xs' variant='secondary'>
|
||||
Fulfilled
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
|
||||
<TableCell className='text-right'>$250.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className='font-medium'>Liam Johnson</div>
|
||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||
liam@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>
|
||||
<Badge className='text-xs' variant='secondary'>
|
||||
Fulfilled
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
|
||||
<TableCell className='text-right'>$250.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className='font-medium'>Olivia Smith</div>
|
||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||
olivia@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>Refund</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>
|
||||
<Badge className='text-xs' variant='outline'>
|
||||
Declined
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className='hidden md:table-cell'>2023-06-24</TableCell>
|
||||
<TableCell className='text-right'>$150.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className='font-medium'>Emma Brown</div>
|
||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||
emma@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
||||
<TableCell className='hidden sm:table-cell'>
|
||||
<Badge className='text-xs' variant='secondary'>
|
||||
Fulfilled
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className='hidden md:table-cell'>2023-06-26</TableCell>
|
||||
<TableCell className='text-right'>$450.00</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div>
|
||||
<Card className='overflow-hidden' x-chunk='dashboard-05-chunk-4'>
|
||||
<CardHeader className='flex flex-row items-start bg-muted/50'>
|
||||
<div className='grid gap-0.5'>
|
||||
<CardTitle className='flex items-center gap-2 text-lg group'>
|
||||
Order Oe31b70H
|
||||
<Button
|
||||
size='icon'
|
||||
variant='outline'
|
||||
className='w-6 h-6 transition-opacity opacity-0 group-hover:opacity-100'
|
||||
>
|
||||
<Copy className='w-3 h-3' />
|
||||
<span className='sr-only'>Copy Order ID</span>
|
||||
</Button>
|
||||
</CardTitle>
|
||||
<CardDescription>Date: November 23, 2023</CardDescription>
|
||||
</div>
|
||||
<div className='flex items-center gap-1 ml-auto'>
|
||||
<Button size='sm' variant='outline' className='h-8 gap-1'>
|
||||
<Truck className='h-3.5 w-3.5' />
|
||||
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
|
||||
Track Order
|
||||
</span>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size='icon' variant='outline' className='w-8 h-8'>
|
||||
<MoreVertical className='h-3.5 w-3.5' />
|
||||
<span className='sr-only'>More</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Export</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Trash</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className='p-6 text-sm'>
|
||||
<div className='grid gap-3'>
|
||||
<div className='font-semibold'>Order Details</div>
|
||||
<ul className='grid gap-3'>
|
||||
<li className='flex items-center justify-between'>
|
||||
<span className='text-muted-foreground'>
|
||||
Glimmer Lamps x <span>2</span>
|
||||
</span>
|
||||
<span>$250.00</span>
|
||||
</li>
|
||||
<li className='flex items-center justify-between'>
|
||||
<span className='text-muted-foreground'>
|
||||
Aqua Filters x <span>1</span>
|
||||
</span>
|
||||
<span>$49.00</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Separator className='my-2' />
|
||||
<ul className='grid gap-3'>
|
||||
<li className='flex items-center justify-between'>
|
||||
<span className='text-muted-foreground'>Subtotal</span>
|
||||
<span>$299.00</span>
|
||||
</li>
|
||||
<li className='flex items-center justify-between'>
|
||||
<span className='text-muted-foreground'>Shipping</span>
|
||||
<span>$5.00</span>
|
||||
</li>
|
||||
<li className='flex items-center justify-between'>
|
||||
<span className='text-muted-foreground'>Tax</span>
|
||||
<span>$25.00</span>
|
||||
</li>
|
||||
<li className='flex items-center justify-between font-semibold'>
|
||||
<span className='text-muted-foreground'>Total</span>
|
||||
<span>$329.00</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Separator className='my-4' />
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div className='grid gap-3'>
|
||||
<div className='font-semibold'>Shipping Information</div>
|
||||
<address className='grid gap-0.5 not-italic text-muted-foreground'>
|
||||
<span>Liam Johnson</span>
|
||||
<span>1234 Main St.</span>
|
||||
<span>Anytown, CA 12345</span>
|
||||
</address>
|
||||
</div>
|
||||
<div className='grid gap-3 auto-rows-max'>
|
||||
<div className='font-semibold'>Billing Information</div>
|
||||
<div className='text-muted-foreground'>Same as shipping address</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className='my-4' />
|
||||
<div className='grid gap-3'>
|
||||
<div className='font-semibold'>Customer Information</div>
|
||||
<dl className='grid gap-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<dt className='text-muted-foreground'>Customer</dt>
|
||||
<dd>Liam Johnson</dd>
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<dt className='text-muted-foreground'>Email</dt>
|
||||
<dd>
|
||||
<a href='mailto:'>liam@acme.com</a>
|
||||
</dd>
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<dt className='text-muted-foreground'>Phone</dt>
|
||||
<dd>
|
||||
<a href='tel:'>+1 234 567 890</a>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<Separator className='my-4' />
|
||||
<div className='grid gap-3'>
|
||||
<div className='font-semibold'>Payment Information</div>
|
||||
<dl className='grid gap-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<dt className='flex items-center gap-1 text-muted-foreground'>
|
||||
<CreditCard className='w-4 h-4' />
|
||||
Visa
|
||||
</dt>
|
||||
<dd>**** **** **** 4532</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className='flex flex-row items-center px-6 py-3 border-t bg-muted/50'>
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
Updated <time dateTime='2023-11-23'>November 23, 2023</time>
|
||||
</div>
|
||||
<Pagination className='w-auto ml-auto mr-0'>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<Button size='icon' variant='outline' className='w-6 h-6'>
|
||||
<ChevronLeft className='h-3.5 w-3.5' />
|
||||
<span className='sr-only'>Previous Order</span>
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<Button size='icon' variant='outline' className='w-6 h-6'>
|
||||
<ChevronRight className='h-3.5 w-3.5' />
|
||||
<span className='sr-only'>Next Order</span>
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</DataTableProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -15,9 +15,6 @@ export const LayoutHeader = () => {
|
||||
<UeckoLogo className='w-24' />
|
||||
<span className='sr-only'>Uecko</span>
|
||||
</Link>
|
||||
<Link to='/home' className='transition-colors text-muted-foreground hover:text-foreground'>
|
||||
<Trans i18nKey='main_menu.home' />
|
||||
</Link>
|
||||
<Link
|
||||
to='/quotes'
|
||||
className='transition-colors text-muted-foreground hover:text-foreground'
|
||||
@ -53,9 +50,6 @@ export const LayoutHeader = () => {
|
||||
<Package2Icon className='w-6 h-6' />
|
||||
<span className='sr-only'>Uecko</span>
|
||||
</Link>
|
||||
<Link to='/home' className='text-muted-foreground hover:text-foreground'>
|
||||
<Trans i18nKey='main_menu.home' />
|
||||
</Link>
|
||||
<Link to='/quotes' className='text-muted-foreground hover:text-foreground'>
|
||||
<Trans i18nKey='main_menu.quotes' />
|
||||
</Link>
|
||||
|
||||
@ -18,6 +18,7 @@ export const defaultAxiosRequestConfig = {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
"Access-Control-Allow-Origin": "*", // Could work and fix the previous problem, but not in all APIs
|
||||
//'api-key': SERVER_API_KEY,
|
||||
},
|
||||
//timeout: 300,
|
||||
|
||||
@ -25,7 +25,7 @@ export const createAxiosAuthActions = (
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
redirectTo: "/home",
|
||||
redirectTo: "/",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
@ -17,7 +17,7 @@ export const useLogin = (params?: UseMutationOptions<AuthActionResponse, Error,
|
||||
onSuccess: (data, variables, context) => {
|
||||
const { success, redirectTo } = data;
|
||||
if (success && redirectTo) {
|
||||
navigate(redirectTo, { replace: true });
|
||||
navigate(redirectTo || "/", { replace: true });
|
||||
}
|
||||
if (onSuccess) {
|
||||
onSuccess(data, variables, context);
|
||||
|
||||
@ -13,10 +13,11 @@ export const useLogout = (params?: UseMutationOptions<AuthActionResponse, Error>
|
||||
return useMutation({
|
||||
mutationKey: keys().auth().action("logout").get(),
|
||||
mutationFn: logout,
|
||||
|
||||
onSuccess: async (data, variables, context) => {
|
||||
const { success, redirectTo } = data;
|
||||
if (success && redirectTo) {
|
||||
navigate(redirectTo);
|
||||
navigate(redirectTo || "/", { replace: true });
|
||||
}
|
||||
if (onSuccess) {
|
||||
onSuccess(data, variables, context);
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
"hide": "Ocultar",
|
||||
"back": "Volver",
|
||||
"upload": "Cargar",
|
||||
"continue": "Continuar",
|
||||
"sort_asc": "Asc",
|
||||
"sort_asc_description": "En order ascendente. Click para ordenar descendentemente.",
|
||||
"sort_desc": "Desc",
|
||||
@ -62,8 +63,8 @@
|
||||
"welcome": "Bienvenido"
|
||||
},
|
||||
"catalog": {
|
||||
"title": "Catálogo de artículos",
|
||||
"list": {
|
||||
"title": "Catálogo de artículos",
|
||||
"columns": {
|
||||
"description": "Descripción",
|
||||
"points": "Puntos",
|
||||
|
||||
@ -10,7 +10,7 @@ import { Result, UniqueID } from "@shared/contexts";
|
||||
import { IDealerRepository } from "../../domain";
|
||||
|
||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||
import { Dealer } from "../../domain/entities/Dealer";
|
||||
import { Dealer } from "../../domain/entities/Dealer/Dealer";
|
||||
|
||||
export interface IGetDealerByUserByUserUseCaseRequest extends IUseCaseRequest {
|
||||
userId: UniqueID;
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
ICreateQuote_Request_DTO,
|
||||
IDomainError,
|
||||
Language,
|
||||
Note,
|
||||
Quantity,
|
||||
Result,
|
||||
UTCDateValue,
|
||||
@ -17,7 +18,7 @@ import {
|
||||
UnitPrice,
|
||||
ensureIdIsValid,
|
||||
} from "@shared/contexts";
|
||||
import { IQuoteRepository, Quote, QuoteItem, QuoteStatus } from "../../domain";
|
||||
import { IQuoteRepository, Quote, QuoteCustomer, QuoteItem, QuoteStatus } from "../../domain";
|
||||
|
||||
export type CreateQuoteResponseOrError =
|
||||
| Result<never, IUseCaseError> // Misc errors (value objects)
|
||||
@ -118,22 +119,47 @@ export class CreateQuoteUseCase
|
||||
return Result.fail(dateOrError.error);
|
||||
}
|
||||
|
||||
const languageOrError = Language.createFromCode(quoteDTO.language_code);
|
||||
const referenceOrError = QuoteStatus.create(quoteDTO.reference);
|
||||
if (referenceOrError.isFailure) {
|
||||
return Result.fail(referenceOrError.error);
|
||||
}
|
||||
|
||||
const languageOrError = Language.createFromCode(quoteDTO.lang_code);
|
||||
if (languageOrError.isFailure) {
|
||||
return Result.fail(languageOrError.error);
|
||||
}
|
||||
|
||||
const customerOrError = QuoteCustomer.create(quoteDTO.customer_information);
|
||||
if (customerOrError.isFailure) {
|
||||
return Result.fail(customerOrError.error);
|
||||
}
|
||||
|
||||
const currencyOrError = Currency.createFromCode(quoteDTO.currency_code);
|
||||
if (currencyOrError.isFailure) {
|
||||
return Result.fail(currencyOrError.error);
|
||||
}
|
||||
|
||||
const paymentOrError = Note.create(quoteDTO.payment_method);
|
||||
if (paymentOrError.isFailure) {
|
||||
return Result.fail(paymentOrError.error);
|
||||
}
|
||||
|
||||
const notesOrError = Note.create(quoteDTO.notes);
|
||||
if (notesOrError.isFailure) {
|
||||
return Result.fail(notesOrError.error);
|
||||
}
|
||||
|
||||
const validityOrError = Note.create(quoteDTO.validity);
|
||||
if (validityOrError.isFailure) {
|
||||
return Result.fail(validityOrError.error);
|
||||
}
|
||||
|
||||
const items = new Collection<QuoteItem>(
|
||||
quoteDTO.items?.map(
|
||||
(item) =>
|
||||
QuoteItem.create({
|
||||
description: Description.create(item.description).object,
|
||||
quantity: Quantity.create(item.quantity).object,
|
||||
quantity: Quantity.create({ amount: item.quantity, precision: 4 }).object,
|
||||
unitPrice: UnitPrice.create({
|
||||
amount: item.unit_price.amount,
|
||||
currencyCode: item.unit_price.currency,
|
||||
@ -147,8 +173,14 @@ export class CreateQuoteUseCase
|
||||
{
|
||||
status: statusOrError.object,
|
||||
date: dateOrError.object,
|
||||
reference: referenceOrError.object,
|
||||
language: languageOrError.object,
|
||||
customer: customerOrError.object,
|
||||
currency: currencyOrError.object,
|
||||
paymentMethod: paymentOrError.object,
|
||||
notes: notesOrError.object,
|
||||
validity: validityOrError.object,
|
||||
|
||||
items,
|
||||
},
|
||||
quoteId
|
||||
|
||||
@ -10,7 +10,7 @@ import { Result, UniqueID } from "@shared/contexts";
|
||||
import { IQuoteRepository } from "../../domain";
|
||||
|
||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||
import { Quote } from "../../domain/entities/Quote";
|
||||
import { Quote } from "../../domain/entities/Quotes/Quote";
|
||||
|
||||
export interface IGetQuoteUseCaseRequest extends IUseCaseRequest {
|
||||
id: UniqueID;
|
||||
|
||||
@ -10,7 +10,7 @@ import { Result, UniqueID } from "@shared/contexts";
|
||||
import { IQuoteRepository } from "../../domain";
|
||||
|
||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||
import { Quote } from "../../domain/entities/Quote";
|
||||
import { Quote } from "../../domain/entities/Quotes/Quote";
|
||||
|
||||
export interface IGetQuoteByUserByUserUseCaseRequest extends IUseCaseRequest {
|
||||
userId: UniqueID;
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export * from "./Dealer";
|
||||
export * from "./DealerRole";
|
||||
export * from "./DealerStatus";
|
||||
@ -4,23 +4,26 @@ import {
|
||||
ICollection,
|
||||
IDomainError,
|
||||
Language,
|
||||
Note,
|
||||
Result,
|
||||
UTCDateValue,
|
||||
UniqueID,
|
||||
} from "@shared/contexts";
|
||||
import { QuoteCustomer } from "./QuoteCustomer";
|
||||
import { QuoteItem } from "./QuoteItem";
|
||||
import { QuoteReference } from "./QuoteReference";
|
||||
import { QuoteStatus } from "./QuoteStatus";
|
||||
|
||||
export interface IQuoteProps {
|
||||
status: QuoteStatus;
|
||||
date: UTCDateValue;
|
||||
reference: string;
|
||||
reference: QuoteReference;
|
||||
customer: QuoteCustomer;
|
||||
language: Language;
|
||||
customer: string;
|
||||
currency: Currency;
|
||||
paymentMethod: string;
|
||||
notes: string;
|
||||
validity: string;
|
||||
paymentMethod: Note;
|
||||
notes: Note;
|
||||
validity: Note;
|
||||
|
||||
items: ICollection<QuoteItem>;
|
||||
}
|
||||
@ -30,9 +33,13 @@ export interface IQuote {
|
||||
|
||||
status: QuoteStatus;
|
||||
date: UTCDateValue;
|
||||
|
||||
reference: QuoteReference;
|
||||
customer: QuoteCustomer;
|
||||
language: Language;
|
||||
currency: Currency;
|
||||
paymentMethod: Note;
|
||||
notes: Note;
|
||||
validity: Note;
|
||||
items: ICollection<QuoteItem>;
|
||||
}
|
||||
|
||||
@ -67,6 +74,14 @@ export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
|
||||
return this.props.status;
|
||||
}
|
||||
|
||||
get reference() {
|
||||
return this.props.reference;
|
||||
}
|
||||
|
||||
get customer() {
|
||||
return this.props.customer;
|
||||
}
|
||||
|
||||
get language() {
|
||||
return this.props.language;
|
||||
}
|
||||
@ -75,6 +90,18 @@ export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
|
||||
return this.props.currency;
|
||||
}
|
||||
|
||||
get paymentMethod() {
|
||||
return this.props.paymentMethod;
|
||||
}
|
||||
|
||||
get notes() {
|
||||
return this.props.notes;
|
||||
}
|
||||
|
||||
get validity() {
|
||||
return this.props.validity;
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this._items;
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import {
|
||||
DomainError,
|
||||
IStringValueObjectOptions,
|
||||
Result,
|
||||
RuleValidator,
|
||||
StringValueObject,
|
||||
handleDomainError,
|
||||
} from "@shared/contexts";
|
||||
import { UndefinedOr } from "@shared/utilities";
|
||||
import Joi from "joi";
|
||||
|
||||
export interface IQuoteCustomerOptions extends IStringValueObjectOptions {}
|
||||
|
||||
export class QuoteCustomer extends StringValueObject {
|
||||
private static readonly MAX_LENGTH = 255;
|
||||
|
||||
protected static validate(value: UndefinedOr<string>, options: IQuoteCustomerOptions) {
|
||||
const rule = Joi.string()
|
||||
.allow(null)
|
||||
.allow("")
|
||||
.default("")
|
||||
.trim()
|
||||
.max(QuoteCustomer.MAX_LENGTH)
|
||||
.label(options.label ? options.label : "value");
|
||||
|
||||
return RuleValidator.validate<string>(rule, value);
|
||||
}
|
||||
|
||||
public static create(value: UndefinedOr<string>, options: IQuoteCustomerOptions = {}) {
|
||||
const _options = {
|
||||
label: "customer",
|
||||
...options,
|
||||
};
|
||||
|
||||
const validationResult = QuoteCustomer.validate(value, _options);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(
|
||||
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(new QuoteCustomer(validationResult.object));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import {
|
||||
DomainError,
|
||||
IStringValueObjectOptions,
|
||||
Result,
|
||||
RuleValidator,
|
||||
StringValueObject,
|
||||
handleDomainError,
|
||||
} from "@shared/contexts";
|
||||
import { UndefinedOr } from "@shared/utilities";
|
||||
import Joi from "joi";
|
||||
|
||||
export interface IQuoteReferenceOptions extends IStringValueObjectOptions {}
|
||||
|
||||
export class QuoteReference extends StringValueObject {
|
||||
private static readonly MAX_LENGTH = 255;
|
||||
|
||||
protected static validate(value: UndefinedOr<string>, options: IQuoteReferenceOptions) {
|
||||
const rule = Joi.string()
|
||||
.allow(null)
|
||||
.allow("")
|
||||
.default("")
|
||||
.trim()
|
||||
.max(QuoteReference.MAX_LENGTH)
|
||||
.label(options.label ? options.label : "value");
|
||||
|
||||
return RuleValidator.validate<string>(rule, value);
|
||||
}
|
||||
|
||||
public static create(value: UndefinedOr<string>, options: IQuoteReferenceOptions = {}) {
|
||||
const _options = {
|
||||
label: "customer",
|
||||
...options,
|
||||
};
|
||||
|
||||
const validationResult = QuoteReference.validate(value, _options);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(
|
||||
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(new QuoteReference(validationResult.object));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export * from "./Quote";
|
||||
export * from "./QuoteCustomer";
|
||||
export * from "./QuoteItem";
|
||||
export * from "./QuoteReference";
|
||||
export * from "./QuoteStatus";
|
||||
@ -1,5 +1,2 @@
|
||||
export * from "./Dealer";
|
||||
export * from "./DealerStatus";
|
||||
export * from "./Quote";
|
||||
export * from "./QuoteItem";
|
||||
export * from "./QuoteStatus";
|
||||
export * from "./Dealer/Dealer";
|
||||
export * from "./Quotes";
|
||||
|
||||
@ -35,6 +35,16 @@ export class CreateQuoteController extends ExpressController {
|
||||
async executeImpl() {
|
||||
try {
|
||||
const quoteDTO: ICreateQuote_Request_DTO = this.req.body;
|
||||
/*const user = <User | undefined>this.req.user;
|
||||
|
||||
if (!user) {
|
||||
const errorMessage = "Unexpected missing user data";
|
||||
const infraError = InfrastructureError.create(
|
||||
InfrastructureError.UNEXCEPTED_ERROR,
|
||||
errorMessage
|
||||
);
|
||||
return this.internalServerError(errorMessage, infraError);
|
||||
}*/
|
||||
|
||||
// Validaciones de DTO
|
||||
const quoteDTOOrError = ensureCreateQuote_Request_DTOIsValid(quoteDTO);
|
||||
|
||||
@ -11,10 +11,12 @@ export const CreateQuotePresenter: ICreateQuotePresenter = {
|
||||
map: (quote: Quote, context: ISalesContext): ICreateQuote_Response_DTO => {
|
||||
return {
|
||||
id: quote.id.toString(),
|
||||
//reference: quote.refe
|
||||
status: quote.status.toString(),
|
||||
date: quote.date.toString(),
|
||||
language_code: quote.date.toString(),
|
||||
lang_code: quote.language.toString(),
|
||||
currency_code: quote.currency.toString(),
|
||||
customer_information: quote.customer,
|
||||
subtotal: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
@ -35,7 +37,7 @@ const quoteItemPresenter = (items: ICollection<QuoteItem>, context: ISalesContex
|
||||
items.totalCount > 0
|
||||
? items.items.map((item: QuoteItem) => ({
|
||||
description: item.description.toString(),
|
||||
quantity: item.quantity.toString(),
|
||||
quantity: item.quantity.toObject(),
|
||||
unit_measure: "",
|
||||
unit_price: {
|
||||
amount: 0,
|
||||
|
||||
@ -2,7 +2,7 @@ import { Currency, Language, UTCDateValue, UniqueID } from "@shared/contexts";
|
||||
|
||||
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
|
||||
import { IQuoteProps, Quote } from "../../domain";
|
||||
import { QuoteStatus } from "../../domain/entities/QuoteStatus";
|
||||
import { QuoteStatus } from "../../domain/entities/Quotes/QuoteStatus";
|
||||
import { ISalesContext } from "../Sales.context";
|
||||
import { QuoteCreationAttributes, Quote_Model } from "../sequelize";
|
||||
import { IQuoteItemMapper, createQuoteItemMapper } from "./quoteItem.mapper";
|
||||
@ -36,6 +36,7 @@ class QuoteMapper
|
||||
date: this.mapsValue(source, "issue_date", UTCDateValue.create),
|
||||
currency: this.mapsValue(source, "quote_currency", Currency.createFromCode),
|
||||
language: this.mapsValue(source, "quote_language", Language.createFromCode),
|
||||
customer: source.customer_information,
|
||||
|
||||
items,
|
||||
};
|
||||
@ -60,7 +61,8 @@ class QuoteMapper
|
||||
status: source.status.toPrimitive(),
|
||||
date: source.date.toPrimitive(),
|
||||
currency_code: source.currency.toPrimitive(),
|
||||
language_code: source.language.toPrimitive(),
|
||||
lang_code: source.language.toPrimitive(),
|
||||
customer_information: source.customer,
|
||||
subtotal: 0,
|
||||
total: 0,
|
||||
items,
|
||||
|
||||
@ -8,8 +8,6 @@ import {
|
||||
} from "@/contexts/sales/infrastructure/express/controllers/dealers";
|
||||
import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware";
|
||||
import Express from "express";
|
||||
import { quoteRoutes } from "./quote.routes";
|
||||
5;
|
||||
|
||||
export const DealerRouter = (appRouter: Express.Router) => {
|
||||
const dealerRoutes: Express.Router = Express.Router({ mergeParams: true });
|
||||
@ -21,7 +19,7 @@ export const DealerRouter = (appRouter: Express.Router) => {
|
||||
dealerRoutes.delete("/:dealerId", checkisAdmin, deleteDealerController);
|
||||
|
||||
// Anidar quotes en /dealers/:dealerId
|
||||
dealerRoutes.use("/:dealerId/quotes", quoteRoutes);
|
||||
//dealerRoutes.use("/:dealerId/quotes", quoteRoutes);
|
||||
|
||||
appRouter.use("/dealers", dealerRoutes);
|
||||
};
|
||||
|
||||
@ -3,5 +3,4 @@ export * from "./catalog.routes";
|
||||
export * from "./dealers.routes";
|
||||
export * from "./profile.routes";
|
||||
export * from "./quote.routes";
|
||||
export * from "./sales.routes";
|
||||
export * from "./users.routes";
|
||||
|
||||
@ -1,20 +1,28 @@
|
||||
import { checkUser } from "@/contexts/auth";
|
||||
import { listQuotesController } from "@/contexts/sales/infrastructure/express/controllers";
|
||||
import {
|
||||
createQuoteController,
|
||||
listQuotesController,
|
||||
} from "@/contexts/sales/infrastructure/express/controllers";
|
||||
import Express from "express";
|
||||
|
||||
export const quoteRoutes: Express.Router = Express.Router({ mergeParams: true });
|
||||
|
||||
quoteRoutes.get(
|
||||
"/",
|
||||
checkUser,
|
||||
(req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
|
||||
listQuotesController(res.locals["context"]).execute(req, res, next)
|
||||
);
|
||||
/*quoteRoutes.get("/:quoteId", isUser, getQuoteMiddleware, getQuoteController);
|
||||
quoteRoutes.post("/", isAdmin, createQuoteController);
|
||||
quoteRoutes.put("/:quoteId", isAdmin, updateQuoteController);
|
||||
quoteRoutes.delete("/:quoteId", isAdmin, deleteQuoteController);*/
|
||||
|
||||
export const QuoteRouter = (appRouter: Express.Router) => {
|
||||
const quoteRoutes: Express.Router = Express.Router({ mergeParams: true });
|
||||
|
||||
quoteRoutes.get(
|
||||
"/",
|
||||
checkUser,
|
||||
(req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
|
||||
listQuotesController(res.locals["context"]).execute(req, res, next)
|
||||
);
|
||||
|
||||
quoteRoutes.post("/", checkUser, createQuoteController);
|
||||
|
||||
//quoteRoutes.put("/:quoteId", checkUser, updateQuoteController);
|
||||
|
||||
/*quoteRoutes.get("/:quoteId", isUser, getQuoteMiddleware, getQuoteController);
|
||||
quoteRoutes.post("/", isAdmin, createQuoteController);
|
||||
|
||||
quoteRoutes.delete("/:quoteId", isAdmin, deleteQuoteController);*/
|
||||
|
||||
appRouter.use("/quotes", quoteRoutes);
|
||||
};
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
import Express from "express";
|
||||
import { DealerRouter } from "./dealers.routes";
|
||||
import { QuoteRouter } from "./quote.routes";
|
||||
|
||||
export const salesRouter = (appRouter: Express.Router) => {
|
||||
DealerRouter(appRouter);
|
||||
QuoteRouter(appRouter);
|
||||
};
|
||||
@ -1,7 +1,13 @@
|
||||
import { salesRouter } from "@/contexts/sales/infrastructure/express";
|
||||
import Express from "express";
|
||||
import { createContextMiddleware } from "./context.middleware";
|
||||
import { authRouter, catalogRouter, profileRouter, usersRouter } from "./routes";
|
||||
import {
|
||||
DealerRouter,
|
||||
authRouter,
|
||||
catalogRouter,
|
||||
profileRouter,
|
||||
quoteRoutes,
|
||||
usersRouter,
|
||||
} from "./routes";
|
||||
|
||||
export const v1Routes = () => {
|
||||
const routes = Express.Router({ mergeParams: true });
|
||||
@ -25,7 +31,8 @@ export const v1Routes = () => {
|
||||
profileRouter(routes);
|
||||
usersRouter(routes);
|
||||
catalogRouter(routes);
|
||||
salesRouter(routes);
|
||||
DealerRouter(routes);
|
||||
quoteRoutes(routes);
|
||||
|
||||
return routes;
|
||||
};
|
||||
|
||||
@ -25,7 +25,7 @@ app.use(responseTime());
|
||||
// enable CORS - Cross Origin Resource Sharing
|
||||
app.use(
|
||||
cors({
|
||||
origin: "http://localhost:5173",
|
||||
origin: "*", //"http://localhost:5173",
|
||||
credentials: true,
|
||||
|
||||
exposedHeaders: [
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import Joi from "joi";
|
||||
|
||||
import { DomainError, Result, RuleValidator, handleDomainError } from "..";
|
||||
import { UndefinedOr } from "../../../../utilities";
|
||||
import { IStringValueObjectOptions, StringValueObject } from "./StringValueObject";
|
||||
|
||||
export class TextValueObject extends StringValueObject {
|
||||
protected static validate(value: UndefinedOr<string>, options: IStringValueObjectOptions) {
|
||||
const rule = Joi.string()
|
||||
.allow(null)
|
||||
.allow("")
|
||||
.default("")
|
||||
.trim()
|
||||
.label(options.label ? options.label : "value");
|
||||
|
||||
return RuleValidator.validate<string>(rule, value);
|
||||
}
|
||||
|
||||
public static create(value: UndefinedOr<string>, options: IStringValueObjectOptions = {}) {
|
||||
const _options = {
|
||||
label: "text",
|
||||
...options,
|
||||
};
|
||||
|
||||
const validationResult = TextValueObject.validate(value, _options);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(
|
||||
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
|
||||
);
|
||||
}
|
||||
return Result.ok(new TextValueObject(validationResult.object));
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,7 @@ export * from "./ResultCollection";
|
||||
export * from "./Slug";
|
||||
export * from "./StringValueObject";
|
||||
export * from "./TINNumber";
|
||||
export * from "./TextValueObject";
|
||||
export * from "./UTCDateValue";
|
||||
export * from "./UniqueID";
|
||||
export * from "./UnitPrice";
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { IMoney_Response_DTO } from "shared/lib/contexts/common";
|
||||
import { IMoney_Response_DTO } from "../../../../../common";
|
||||
|
||||
export interface ICreateQuote_Response_DTO {
|
||||
id: string;
|
||||
status: string;
|
||||
date: string;
|
||||
language_code: string;
|
||||
lang_code: string;
|
||||
currency_code: string;
|
||||
|
||||
subtotal: IMoney_Response_DTO;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user