.
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
|
VITE_API_KEY=e175f809ba71fb2765ad5e60f9d77596-es19
|
||||||
@ -5,12 +5,12 @@ import {
|
|||||||
LoginPage,
|
LoginPage,
|
||||||
LogoutPage,
|
LogoutPage,
|
||||||
QuoteCreate,
|
QuoteCreate,
|
||||||
|
QuoteEdit,
|
||||||
SettingsEditor,
|
SettingsEditor,
|
||||||
SettingsLayout,
|
SettingsLayout,
|
||||||
StartPage,
|
StartPage,
|
||||||
} from "./app";
|
} from "./app";
|
||||||
import { CatalogLayout, CatalogList } from "./app/catalog";
|
import { CatalogLayout, CatalogList } from "./app/catalog";
|
||||||
import { DashboardPage } from "./app/dashboard";
|
|
||||||
import { QuotesLayout } from "./app/quotes/layout";
|
import { QuotesLayout } from "./app/quotes/layout";
|
||||||
import { QuotesList } from "./app/quotes/list";
|
import { QuotesList } from "./app/quotes/list";
|
||||||
import { ProtectedRoute } from "./components";
|
import { ProtectedRoute } from "./components";
|
||||||
@ -26,14 +26,6 @@ export const Routes = () => {
|
|||||||
|
|
||||||
// Define routes accessible only to authenticated users
|
// Define routes accessible only to authenticated users
|
||||||
const routesForAuthenticatedOnly = [
|
const routesForAuthenticatedOnly = [
|
||||||
{
|
|
||||||
path: "/home",
|
|
||||||
element: (
|
|
||||||
<ProtectedRoute>
|
|
||||||
<DashboardPage />
|
|
||||||
</ProtectedRoute>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/catalog",
|
path: "/catalog",
|
||||||
element: (
|
element: (
|
||||||
@ -84,6 +76,10 @@ export const Routes = () => {
|
|||||||
path: "add",
|
path: "add",
|
||||||
element: <QuoteCreate />,
|
element: <QuoteCreate />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
element: <QuoteEdit />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export const ErrorPage = (props: ErrorPageProps) => {
|
|||||||
<Button id='backButton' onClick={() => navigate(-1)}>
|
<Button id='backButton' onClick={() => navigate(-1)}>
|
||||||
Return to Previous Page
|
Return to Previous Page
|
||||||
</Button>
|
</Button>
|
||||||
<Button id='homeButton' onClick={() => navigate("/home")}>
|
<Button id='homeButton' onClick={() => navigate("/")}>
|
||||||
Return to Home Page
|
Return to Home Page
|
||||||
</Button>
|
</Button>
|
||||||
<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 { Layout, LayoutContent, LayoutHeader } from "@/components";
|
||||||
import { PropsWithChildren } from "react";
|
import { PropsWithChildren } from "react";
|
||||||
import { Trans } from "react-i18next";
|
|
||||||
import { CatalogProvider } from "./CatalogContext";
|
import { CatalogProvider } from "./CatalogContext";
|
||||||
|
|
||||||
export const CatalogLayout = ({ children }: PropsWithChildren) => {
|
export const CatalogLayout = ({ children }: PropsWithChildren) => {
|
||||||
@ -8,14 +7,7 @@ export const CatalogLayout = ({ children }: PropsWithChildren) => {
|
|||||||
<CatalogProvider>
|
<CatalogProvider>
|
||||||
<Layout>
|
<Layout>
|
||||||
<LayoutHeader />
|
<LayoutHeader />
|
||||||
<LayoutContent>
|
<LayoutContent>{children}</LayoutContent>
|
||||||
<div className='flex items-center'>
|
|
||||||
<h1 className='text-lg font-semibold md:text-2xl'>
|
|
||||||
<Trans i18nKey='catalog.title' />
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</LayoutContent>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</CatalogProvider>
|
</CatalogProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,9 +1,19 @@
|
|||||||
import { DataTableProvider } from "@/lib/hooks";
|
import { DataTableProvider } from "@/lib/hooks";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
import { CatalogDataTable } from "./components";
|
import { CatalogDataTable } from "./components";
|
||||||
|
|
||||||
export const CatalogList = () => {
|
export const CatalogList = () => {
|
||||||
return (
|
return (
|
||||||
<DataTableProvider>
|
<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 />
|
<CatalogDataTable />
|
||||||
</DataTableProvider>
|
</DataTableProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import {
|
|||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { Input } from "@/ui";
|
import { Input } from "@/ui";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { HashIcon } from "lucide-react";
|
|
||||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||||
import { useDetailColumns } from "../../hooks";
|
import { useDetailColumns } from "../../hooks";
|
||||||
import { SortableDataTable } from "../SortableDataTable";
|
import { SortableDataTable } from "../SortableDataTable";
|
||||||
@ -24,36 +23,22 @@ export const QuoteDetailsCardEditor = () => {
|
|||||||
|
|
||||||
const columns = useDetailColumns(
|
const columns = useDetailColumns(
|
||||||
[
|
[
|
||||||
{
|
/*{
|
||||||
id: "row_id" as const,
|
id: "row_id" as const,
|
||||||
header: () => (
|
header: () => (
|
||||||
<HashIcon aria-label='Orden de fila' className='items-center justify-center w-4 h-4' />
|
<HashIcon aria-label='Orden de fila' className='items-center justify-center w-4 h-4' />
|
||||||
),
|
),
|
||||||
accessorFn: (originalRow: unknown, index: number) => index + 1,
|
accessorFn: (originalRow: unknown, index: number) => index + 1,
|
||||||
size: 26,
|
size: 5,
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
enableResizing: 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,
|
id: "quantity" as const,
|
||||||
accessorKey: "quantity",
|
accessorKey: "quantity",
|
||||||
header: "quantity",
|
header: "quantity",
|
||||||
size: 60,
|
size: 5,
|
||||||
cell: ({ row: { index }, column: { id } }) => {
|
cell: ({ row: { index }, column: { id } }) => {
|
||||||
return (
|
return (
|
||||||
<FormTextField
|
<FormTextField
|
||||||
@ -65,37 +50,55 @@ export const QuoteDetailsCardEditor = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "unit_measure" as const,
|
id: "description" as const,
|
||||||
accessorKey: "unit_measure",
|
accessorKey: "description",
|
||||||
header: "unit_measure",
|
|
||||||
size: 60,
|
|
||||||
cell: ({ row: { index }, column: { id } }) => {
|
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,
|
id: "price" as const,
|
||||||
accessorKey: "unit_price",
|
accessorKey: "price",
|
||||||
header: "unit_price",
|
header: "price",
|
||||||
|
size: 10,
|
||||||
cell: ({ row: { index }, column: { id } }) => {
|
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,
|
id: "discount" as const,
|
||||||
accessorKey: "tax_amount",
|
accessorKey: "discount",
|
||||||
header: "tax_amount",
|
header: "discount",
|
||||||
|
size: 5,
|
||||||
|
cell: ({ row: { index }, column: { id } }) => {
|
||||||
|
return <FormMoneyField control={control} {...register(`items.${index}.discount`)} />;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "total" as const,
|
id: "total" as const,
|
||||||
accessorKey: "total",
|
accessorKey: "total",
|
||||||
header: "total",
|
header: "total",
|
||||||
},*/,
|
size: 10,
|
||||||
|
cell: ({ row: { index }, column: { id } }) => {
|
||||||
|
return <FormMoneyField control={control} {...register(`items.${index}.total`)} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
enableDragHandleColumn: true,
|
enableDragHandleColumn: true,
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
|
import { CancelButton, FormDatePickerField, FormTextAreaField, FormTextField } from "@/components";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
import { ChevronLeft } from "lucide-react";
|
import { ChevronLeft } from "lucide-react";
|
||||||
|
|
||||||
import { SubmitButton } from "@/components";
|
import { SubmitButton } from "@/components";
|
||||||
import { useGetIdentity } from "@/lib/hooks";
|
import { Button, Form } from "@/ui";
|
||||||
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 { SubmitHandler, useForm } from "react-hook-form";
|
||||||
import { QuoteGeneralCardEditor } from "./components/editors";
|
|
||||||
import { useQuotes } from "./hooks";
|
import { useQuotes } from "./hooks";
|
||||||
|
|
||||||
type QuoteDataForm = {
|
type QuoteDataForm = {
|
||||||
@ -23,45 +22,35 @@ type QuoteDataForm = {
|
|||||||
items: any[];
|
items: any[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type QuoteCreateProps = {
|
/*type QuoteCreateProps = {
|
||||||
isOverModal?: boolean;
|
isOverModal?: boolean;
|
||||||
};
|
};*/
|
||||||
|
|
||||||
export const QuoteCreate = ({ isOverModal }: QuoteCreateProps) => {
|
export const QuoteCreate = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
//const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const { data: userIdentity } = useGetIdentity();
|
//const { data: userIdentity } = useGetIdentity();
|
||||||
console.log(userIdentity);
|
//console.log(userIdentity);
|
||||||
|
|
||||||
const { useQuery, useMutation } = useQuotes();
|
const { useMutation } = useQuotes();
|
||||||
|
|
||||||
const { data } = useQuery;
|
|
||||||
const { mutate } = useMutation;
|
const { mutate } = useMutation;
|
||||||
|
|
||||||
const form = useForm<QuoteDataForm>({
|
const form = useForm<QuoteDataForm>({
|
||||||
mode: "onBlur",
|
|
||||||
values: data,
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
date: "",
|
|
||||||
reference: "",
|
reference: "",
|
||||||
|
date: Date.now().toLocaleString(),
|
||||||
customer_information: "",
|
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 {
|
try {
|
||||||
setLoading(true);
|
//setLoading(true);
|
||||||
data.currency_code = "EUR";
|
mutate(formData);
|
||||||
data.lang_code = String(userIdentity?.language);
|
|
||||||
mutate(data);
|
|
||||||
} finally {
|
} 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'>
|
<h1 className='flex-1 text-xl font-semibold tracking-tight shrink-0 whitespace-nowrap sm:grow-0'>
|
||||||
{t("quotes.create.title")}
|
{t("quotes.create.title")}
|
||||||
</h1>
|
</h1>
|
||||||
<Badge variant='default' className='ml-auto sm:ml-0'>
|
</div>
|
||||||
{t("quotes.status.draft")}
|
|
||||||
</Badge>
|
<div className='grid max-w-lg gap-6'>
|
||||||
<div className='items-center hidden gap-2 md:ml-auto md:flex'>
|
<FormTextField
|
||||||
<Button variant='outline' size='sm'>
|
label={t("quotes.create.form_fields.reference.label")}
|
||||||
{t("quotes.create.buttons.discard")}
|
description={t("quotes.create.form_fields.reference.desc")}
|
||||||
</Button>
|
disabled={form.formState.disabled}
|
||||||
<SubmitButton variant={form.formState.isDirty ? "default" : "outline"} size='sm'>
|
placeholder={t("quotes.create.form_fields.reference.placeholder")}
|
||||||
{t("quotes.create.buttons.save_quote")}
|
{...form.register("reference", {
|
||||||
</SubmitButton>
|
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>
|
||||||
</div>
|
</div>
|
||||||
<Tabs defaultValue='general' className='space-y-4'>
|
|
||||||
<TabsList>
|
<div className='flex items-center justify-center gap-2'>
|
||||||
<TabsTrigger value='general'>{t("quotes.create.tabs.general")}</TabsTrigger>
|
<CancelButton
|
||||||
<TabsTrigger value='items'>{t("quotes.create.tabs.items")}</TabsTrigger>
|
variant='outline'
|
||||||
<TabsTrigger value='documents'>{t("quotes.create.tabs.documents")}</TabsTrigger>
|
size='sm'
|
||||||
<TabsTrigger value='history'>{t("quotes.create.tabs.history")}</TabsTrigger>
|
label={t("quotes.create.buttons.discard")}
|
||||||
</TabsList>
|
></CancelButton>
|
||||||
<TabsContent value='general'>
|
|
||||||
<QuoteGeneralCardEditor />
|
<SubmitButton size='sm' label={t("common.continue")}></SubmitButton>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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();
|
id = UniqueID.generateNewID().object.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataSource.updateOne({
|
return dataSource.createOne({
|
||||||
resource: "quotes",
|
resource: "quotes",
|
||||||
data,
|
data: {
|
||||||
id,
|
...data,
|
||||||
|
id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./create";
|
export * from "./create";
|
||||||
|
export * from "./edit";
|
||||||
export * from "./list";
|
export * from "./list";
|
||||||
|
|||||||
@ -1,15 +1,443 @@
|
|||||||
import { DataTableProvider } from "@/lib/hooks";
|
import { DataTableProvider } from "@/lib/hooks";
|
||||||
|
import {
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Copy,
|
||||||
|
CreditCard,
|
||||||
|
File,
|
||||||
|
ListFilter,
|
||||||
|
MoreVertical,
|
||||||
|
Truck,
|
||||||
|
} from "lucide-react";
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
import { QuotesDataTable } from "./components";
|
import { QuotesDataTable } from "./components";
|
||||||
|
|
||||||
export const QuotesList = () => (
|
import {
|
||||||
<DataTableProvider>
|
Badge,
|
||||||
<div className='flex items-center'>
|
Button,
|
||||||
<h1 className='text-lg font-semibold md:text-2xl'>
|
Card,
|
||||||
<Trans i18nKey='quotes.title' />
|
CardContent,
|
||||||
</h1>
|
CardDescription,
|
||||||
</div>
|
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 />
|
export const QuotesList = () => {
|
||||||
</DataTableProvider>
|
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' />
|
<UeckoLogo className='w-24' />
|
||||||
<span className='sr-only'>Uecko</span>
|
<span className='sr-only'>Uecko</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to='/home' className='transition-colors text-muted-foreground hover:text-foreground'>
|
|
||||||
<Trans i18nKey='main_menu.home' />
|
|
||||||
</Link>
|
|
||||||
<Link
|
<Link
|
||||||
to='/quotes'
|
to='/quotes'
|
||||||
className='transition-colors text-muted-foreground hover:text-foreground'
|
className='transition-colors text-muted-foreground hover:text-foreground'
|
||||||
@ -53,9 +50,6 @@ export const LayoutHeader = () => {
|
|||||||
<Package2Icon className='w-6 h-6' />
|
<Package2Icon className='w-6 h-6' />
|
||||||
<span className='sr-only'>Uecko</span>
|
<span className='sr-only'>Uecko</span>
|
||||||
</Link>
|
</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'>
|
<Link to='/quotes' className='text-muted-foreground hover:text-foreground'>
|
||||||
<Trans i18nKey='main_menu.quotes' />
|
<Trans i18nKey='main_menu.quotes' />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export const defaultAxiosRequestConfig = {
|
|||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
"Cache-Control": "no-cache",
|
"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,
|
//'api-key': SERVER_API_KEY,
|
||||||
},
|
},
|
||||||
//timeout: 300,
|
//timeout: 300,
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export const createAxiosAuthActions = (
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data,
|
data,
|
||||||
redirectTo: "/home",
|
redirectTo: "/",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -17,7 +17,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, { replace: true });
|
navigate(redirectTo || "/", { replace: true });
|
||||||
}
|
}
|
||||||
if (onSuccess) {
|
if (onSuccess) {
|
||||||
onSuccess(data, variables, context);
|
onSuccess(data, variables, context);
|
||||||
|
|||||||
@ -13,10 +13,11 @@ export const useLogout = (params?: UseMutationOptions<AuthActionResponse, Error>
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: keys().auth().action("logout").get(),
|
mutationKey: keys().auth().action("logout").get(),
|
||||||
mutationFn: logout,
|
mutationFn: logout,
|
||||||
|
|
||||||
onSuccess: async (data, variables, context) => {
|
onSuccess: async (data, variables, context) => {
|
||||||
const { success, redirectTo } = data;
|
const { success, redirectTo } = data;
|
||||||
if (success && redirectTo) {
|
if (success && redirectTo) {
|
||||||
navigate(redirectTo);
|
navigate(redirectTo || "/", { replace: true });
|
||||||
}
|
}
|
||||||
if (onSuccess) {
|
if (onSuccess) {
|
||||||
onSuccess(data, variables, context);
|
onSuccess(data, variables, context);
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
"hide": "Ocultar",
|
"hide": "Ocultar",
|
||||||
"back": "Volver",
|
"back": "Volver",
|
||||||
"upload": "Cargar",
|
"upload": "Cargar",
|
||||||
|
"continue": "Continuar",
|
||||||
"sort_asc": "Asc",
|
"sort_asc": "Asc",
|
||||||
"sort_asc_description": "En order ascendente. Click para ordenar descendentemente.",
|
"sort_asc_description": "En order ascendente. Click para ordenar descendentemente.",
|
||||||
"sort_desc": "Desc",
|
"sort_desc": "Desc",
|
||||||
@ -62,8 +63,8 @@
|
|||||||
"welcome": "Bienvenido"
|
"welcome": "Bienvenido"
|
||||||
},
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"title": "Catálogo de artículos",
|
|
||||||
"list": {
|
"list": {
|
||||||
|
"title": "Catálogo de artículos",
|
||||||
"columns": {
|
"columns": {
|
||||||
"description": "Descripción",
|
"description": "Descripción",
|
||||||
"points": "Puntos",
|
"points": "Puntos",
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { Result, UniqueID } from "@shared/contexts";
|
|||||||
import { IDealerRepository } from "../../domain";
|
import { IDealerRepository } from "../../domain";
|
||||||
|
|
||||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||||
import { Dealer } from "../../domain/entities/Dealer";
|
import { Dealer } from "../../domain/entities/Dealer/Dealer";
|
||||||
|
|
||||||
export interface IGetDealerByUserByUserUseCaseRequest extends IUseCaseRequest {
|
export interface IGetDealerByUserByUserUseCaseRequest extends IUseCaseRequest {
|
||||||
userId: UniqueID;
|
userId: UniqueID;
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
ICreateQuote_Request_DTO,
|
ICreateQuote_Request_DTO,
|
||||||
IDomainError,
|
IDomainError,
|
||||||
Language,
|
Language,
|
||||||
|
Note,
|
||||||
Quantity,
|
Quantity,
|
||||||
Result,
|
Result,
|
||||||
UTCDateValue,
|
UTCDateValue,
|
||||||
@ -17,7 +18,7 @@ import {
|
|||||||
UnitPrice,
|
UnitPrice,
|
||||||
ensureIdIsValid,
|
ensureIdIsValid,
|
||||||
} from "@shared/contexts";
|
} from "@shared/contexts";
|
||||||
import { IQuoteRepository, Quote, QuoteItem, QuoteStatus } from "../../domain";
|
import { IQuoteRepository, Quote, QuoteCustomer, QuoteItem, QuoteStatus } from "../../domain";
|
||||||
|
|
||||||
export type CreateQuoteResponseOrError =
|
export type CreateQuoteResponseOrError =
|
||||||
| Result<never, IUseCaseError> // Misc errors (value objects)
|
| Result<never, IUseCaseError> // Misc errors (value objects)
|
||||||
@ -118,22 +119,47 @@ export class CreateQuoteUseCase
|
|||||||
return Result.fail(dateOrError.error);
|
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) {
|
if (languageOrError.isFailure) {
|
||||||
return Result.fail(languageOrError.error);
|
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);
|
const currencyOrError = Currency.createFromCode(quoteDTO.currency_code);
|
||||||
if (currencyOrError.isFailure) {
|
if (currencyOrError.isFailure) {
|
||||||
return Result.fail(currencyOrError.error);
|
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>(
|
const items = new Collection<QuoteItem>(
|
||||||
quoteDTO.items?.map(
|
quoteDTO.items?.map(
|
||||||
(item) =>
|
(item) =>
|
||||||
QuoteItem.create({
|
QuoteItem.create({
|
||||||
description: Description.create(item.description).object,
|
description: Description.create(item.description).object,
|
||||||
quantity: Quantity.create(item.quantity).object,
|
quantity: Quantity.create({ amount: item.quantity, precision: 4 }).object,
|
||||||
unitPrice: UnitPrice.create({
|
unitPrice: UnitPrice.create({
|
||||||
amount: item.unit_price.amount,
|
amount: item.unit_price.amount,
|
||||||
currencyCode: item.unit_price.currency,
|
currencyCode: item.unit_price.currency,
|
||||||
@ -147,8 +173,14 @@ export class CreateQuoteUseCase
|
|||||||
{
|
{
|
||||||
status: statusOrError.object,
|
status: statusOrError.object,
|
||||||
date: dateOrError.object,
|
date: dateOrError.object,
|
||||||
|
reference: referenceOrError.object,
|
||||||
language: languageOrError.object,
|
language: languageOrError.object,
|
||||||
|
customer: customerOrError.object,
|
||||||
currency: currencyOrError.object,
|
currency: currencyOrError.object,
|
||||||
|
paymentMethod: paymentOrError.object,
|
||||||
|
notes: notesOrError.object,
|
||||||
|
validity: validityOrError.object,
|
||||||
|
|
||||||
items,
|
items,
|
||||||
},
|
},
|
||||||
quoteId
|
quoteId
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { Result, UniqueID } from "@shared/contexts";
|
|||||||
import { IQuoteRepository } from "../../domain";
|
import { IQuoteRepository } from "../../domain";
|
||||||
|
|
||||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||||
import { Quote } from "../../domain/entities/Quote";
|
import { Quote } from "../../domain/entities/Quotes/Quote";
|
||||||
|
|
||||||
export interface IGetQuoteUseCaseRequest extends IUseCaseRequest {
|
export interface IGetQuoteUseCaseRequest extends IUseCaseRequest {
|
||||||
id: UniqueID;
|
id: UniqueID;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { Result, UniqueID } from "@shared/contexts";
|
|||||||
import { IQuoteRepository } from "../../domain";
|
import { IQuoteRepository } from "../../domain";
|
||||||
|
|
||||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||||
import { Quote } from "../../domain/entities/Quote";
|
import { Quote } from "../../domain/entities/Quotes/Quote";
|
||||||
|
|
||||||
export interface IGetQuoteByUserByUserUseCaseRequest extends IUseCaseRequest {
|
export interface IGetQuoteByUserByUserUseCaseRequest extends IUseCaseRequest {
|
||||||
userId: UniqueID;
|
userId: UniqueID;
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./Dealer";
|
||||||
|
export * from "./DealerRole";
|
||||||
|
export * from "./DealerStatus";
|
||||||
@ -4,23 +4,26 @@ import {
|
|||||||
ICollection,
|
ICollection,
|
||||||
IDomainError,
|
IDomainError,
|
||||||
Language,
|
Language,
|
||||||
|
Note,
|
||||||
Result,
|
Result,
|
||||||
UTCDateValue,
|
UTCDateValue,
|
||||||
UniqueID,
|
UniqueID,
|
||||||
} from "@shared/contexts";
|
} from "@shared/contexts";
|
||||||
|
import { QuoteCustomer } from "./QuoteCustomer";
|
||||||
import { QuoteItem } from "./QuoteItem";
|
import { QuoteItem } from "./QuoteItem";
|
||||||
|
import { QuoteReference } from "./QuoteReference";
|
||||||
import { QuoteStatus } from "./QuoteStatus";
|
import { QuoteStatus } from "./QuoteStatus";
|
||||||
|
|
||||||
export interface IQuoteProps {
|
export interface IQuoteProps {
|
||||||
status: QuoteStatus;
|
status: QuoteStatus;
|
||||||
date: UTCDateValue;
|
date: UTCDateValue;
|
||||||
reference: string;
|
reference: QuoteReference;
|
||||||
|
customer: QuoteCustomer;
|
||||||
language: Language;
|
language: Language;
|
||||||
customer: string;
|
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
paymentMethod: string;
|
paymentMethod: Note;
|
||||||
notes: string;
|
notes: Note;
|
||||||
validity: string;
|
validity: Note;
|
||||||
|
|
||||||
items: ICollection<QuoteItem>;
|
items: ICollection<QuoteItem>;
|
||||||
}
|
}
|
||||||
@ -30,9 +33,13 @@ export interface IQuote {
|
|||||||
|
|
||||||
status: QuoteStatus;
|
status: QuoteStatus;
|
||||||
date: UTCDateValue;
|
date: UTCDateValue;
|
||||||
|
reference: QuoteReference;
|
||||||
|
customer: QuoteCustomer;
|
||||||
language: Language;
|
language: Language;
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
|
paymentMethod: Note;
|
||||||
|
notes: Note;
|
||||||
|
validity: Note;
|
||||||
items: ICollection<QuoteItem>;
|
items: ICollection<QuoteItem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +74,14 @@ export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
|
|||||||
return this.props.status;
|
return this.props.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get reference() {
|
||||||
|
return this.props.reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
get customer() {
|
||||||
|
return this.props.customer;
|
||||||
|
}
|
||||||
|
|
||||||
get language() {
|
get language() {
|
||||||
return this.props.language;
|
return this.props.language;
|
||||||
}
|
}
|
||||||
@ -75,6 +90,18 @@ export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
|
|||||||
return this.props.currency;
|
return this.props.currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get paymentMethod() {
|
||||||
|
return this.props.paymentMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
get notes() {
|
||||||
|
return this.props.notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
get validity() {
|
||||||
|
return this.props.validity;
|
||||||
|
}
|
||||||
|
|
||||||
get items() {
|
get items() {
|
||||||
return this._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 "./Dealer/Dealer";
|
||||||
export * from "./DealerStatus";
|
export * from "./Quotes";
|
||||||
export * from "./Quote";
|
|
||||||
export * from "./QuoteItem";
|
|
||||||
export * from "./QuoteStatus";
|
|
||||||
|
|||||||
@ -35,6 +35,16 @@ export class CreateQuoteController extends ExpressController {
|
|||||||
async executeImpl() {
|
async executeImpl() {
|
||||||
try {
|
try {
|
||||||
const quoteDTO: ICreateQuote_Request_DTO = this.req.body;
|
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
|
// Validaciones de DTO
|
||||||
const quoteDTOOrError = ensureCreateQuote_Request_DTOIsValid(quoteDTO);
|
const quoteDTOOrError = ensureCreateQuote_Request_DTOIsValid(quoteDTO);
|
||||||
|
|||||||
@ -11,10 +11,12 @@ export const CreateQuotePresenter: ICreateQuotePresenter = {
|
|||||||
map: (quote: Quote, context: ISalesContext): ICreateQuote_Response_DTO => {
|
map: (quote: Quote, context: ISalesContext): ICreateQuote_Response_DTO => {
|
||||||
return {
|
return {
|
||||||
id: quote.id.toString(),
|
id: quote.id.toString(),
|
||||||
|
//reference: quote.refe
|
||||||
status: quote.status.toString(),
|
status: quote.status.toString(),
|
||||||
date: quote.date.toString(),
|
date: quote.date.toString(),
|
||||||
language_code: quote.date.toString(),
|
lang_code: quote.language.toString(),
|
||||||
currency_code: quote.currency.toString(),
|
currency_code: quote.currency.toString(),
|
||||||
|
customer_information: quote.customer,
|
||||||
subtotal: {
|
subtotal: {
|
||||||
amount: 0,
|
amount: 0,
|
||||||
precision: 2,
|
precision: 2,
|
||||||
@ -35,7 +37,7 @@ const quoteItemPresenter = (items: ICollection<QuoteItem>, context: ISalesContex
|
|||||||
items.totalCount > 0
|
items.totalCount > 0
|
||||||
? items.items.map((item: QuoteItem) => ({
|
? items.items.map((item: QuoteItem) => ({
|
||||||
description: item.description.toString(),
|
description: item.description.toString(),
|
||||||
quantity: item.quantity.toString(),
|
quantity: item.quantity.toObject(),
|
||||||
unit_measure: "",
|
unit_measure: "",
|
||||||
unit_price: {
|
unit_price: {
|
||||||
amount: 0,
|
amount: 0,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Currency, Language, UTCDateValue, UniqueID } from "@shared/contexts";
|
|||||||
|
|
||||||
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
|
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
|
||||||
import { IQuoteProps, Quote } from "../../domain";
|
import { IQuoteProps, Quote } from "../../domain";
|
||||||
import { QuoteStatus } from "../../domain/entities/QuoteStatus";
|
import { QuoteStatus } from "../../domain/entities/Quotes/QuoteStatus";
|
||||||
import { ISalesContext } from "../Sales.context";
|
import { ISalesContext } from "../Sales.context";
|
||||||
import { QuoteCreationAttributes, Quote_Model } from "../sequelize";
|
import { QuoteCreationAttributes, Quote_Model } from "../sequelize";
|
||||||
import { IQuoteItemMapper, createQuoteItemMapper } from "./quoteItem.mapper";
|
import { IQuoteItemMapper, createQuoteItemMapper } from "./quoteItem.mapper";
|
||||||
@ -36,6 +36,7 @@ class QuoteMapper
|
|||||||
date: this.mapsValue(source, "issue_date", UTCDateValue.create),
|
date: this.mapsValue(source, "issue_date", UTCDateValue.create),
|
||||||
currency: this.mapsValue(source, "quote_currency", Currency.createFromCode),
|
currency: this.mapsValue(source, "quote_currency", Currency.createFromCode),
|
||||||
language: this.mapsValue(source, "quote_language", Language.createFromCode),
|
language: this.mapsValue(source, "quote_language", Language.createFromCode),
|
||||||
|
customer: source.customer_information,
|
||||||
|
|
||||||
items,
|
items,
|
||||||
};
|
};
|
||||||
@ -60,7 +61,8 @@ class QuoteMapper
|
|||||||
status: source.status.toPrimitive(),
|
status: source.status.toPrimitive(),
|
||||||
date: source.date.toPrimitive(),
|
date: source.date.toPrimitive(),
|
||||||
currency_code: source.currency.toPrimitive(),
|
currency_code: source.currency.toPrimitive(),
|
||||||
language_code: source.language.toPrimitive(),
|
lang_code: source.language.toPrimitive(),
|
||||||
|
customer_information: source.customer,
|
||||||
subtotal: 0,
|
subtotal: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
items,
|
items,
|
||||||
|
|||||||
@ -8,8 +8,6 @@ import {
|
|||||||
} from "@/contexts/sales/infrastructure/express/controllers/dealers";
|
} from "@/contexts/sales/infrastructure/express/controllers/dealers";
|
||||||
import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware";
|
import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware";
|
||||||
import Express from "express";
|
import Express from "express";
|
||||||
import { quoteRoutes } from "./quote.routes";
|
|
||||||
5;
|
|
||||||
|
|
||||||
export const DealerRouter = (appRouter: Express.Router) => {
|
export const DealerRouter = (appRouter: Express.Router) => {
|
||||||
const dealerRoutes: Express.Router = Express.Router({ mergeParams: true });
|
const dealerRoutes: Express.Router = Express.Router({ mergeParams: true });
|
||||||
@ -21,7 +19,7 @@ export const DealerRouter = (appRouter: Express.Router) => {
|
|||||||
dealerRoutes.delete("/:dealerId", checkisAdmin, deleteDealerController);
|
dealerRoutes.delete("/:dealerId", checkisAdmin, deleteDealerController);
|
||||||
|
|
||||||
// Anidar quotes en /dealers/:dealerId
|
// Anidar quotes en /dealers/:dealerId
|
||||||
dealerRoutes.use("/:dealerId/quotes", quoteRoutes);
|
//dealerRoutes.use("/:dealerId/quotes", quoteRoutes);
|
||||||
|
|
||||||
appRouter.use("/dealers", dealerRoutes);
|
appRouter.use("/dealers", dealerRoutes);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,5 +3,4 @@ export * from "./catalog.routes";
|
|||||||
export * from "./dealers.routes";
|
export * from "./dealers.routes";
|
||||||
export * from "./profile.routes";
|
export * from "./profile.routes";
|
||||||
export * from "./quote.routes";
|
export * from "./quote.routes";
|
||||||
export * from "./sales.routes";
|
|
||||||
export * from "./users.routes";
|
export * from "./users.routes";
|
||||||
|
|||||||
@ -1,20 +1,28 @@
|
|||||||
import { checkUser } from "@/contexts/auth";
|
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";
|
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) => {
|
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);
|
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 Express from "express";
|
||||||
import { createContextMiddleware } from "./context.middleware";
|
import { createContextMiddleware } from "./context.middleware";
|
||||||
import { authRouter, catalogRouter, profileRouter, usersRouter } from "./routes";
|
import {
|
||||||
|
DealerRouter,
|
||||||
|
authRouter,
|
||||||
|
catalogRouter,
|
||||||
|
profileRouter,
|
||||||
|
quoteRoutes,
|
||||||
|
usersRouter,
|
||||||
|
} from "./routes";
|
||||||
|
|
||||||
export const v1Routes = () => {
|
export const v1Routes = () => {
|
||||||
const routes = Express.Router({ mergeParams: true });
|
const routes = Express.Router({ mergeParams: true });
|
||||||
@ -25,7 +31,8 @@ export const v1Routes = () => {
|
|||||||
profileRouter(routes);
|
profileRouter(routes);
|
||||||
usersRouter(routes);
|
usersRouter(routes);
|
||||||
catalogRouter(routes);
|
catalogRouter(routes);
|
||||||
salesRouter(routes);
|
DealerRouter(routes);
|
||||||
|
quoteRoutes(routes);
|
||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,7 +25,7 @@ app.use(responseTime());
|
|||||||
// enable CORS - Cross Origin Resource Sharing
|
// enable CORS - Cross Origin Resource Sharing
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
origin: "http://localhost:5173",
|
origin: "*", //"http://localhost:5173",
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
|
||||||
exposedHeaders: [
|
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 "./Slug";
|
||||||
export * from "./StringValueObject";
|
export * from "./StringValueObject";
|
||||||
export * from "./TINNumber";
|
export * from "./TINNumber";
|
||||||
|
export * from "./TextValueObject";
|
||||||
export * from "./UTCDateValue";
|
export * from "./UTCDateValue";
|
||||||
export * from "./UniqueID";
|
export * from "./UniqueID";
|
||||||
export * from "./UnitPrice";
|
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 {
|
export interface ICreateQuote_Response_DTO {
|
||||||
id: string;
|
id: string;
|
||||||
status: string;
|
status: string;
|
||||||
date: string;
|
date: string;
|
||||||
language_code: string;
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
|
||||||
subtotal: IMoney_Response_DTO;
|
subtotal: IMoney_Response_DTO;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user