This commit is contained in:
David Arranz 2024-07-01 19:12:15 +02:00
parent d5281fb1f5
commit 0b00b84289
45 changed files with 961 additions and 192 deletions

View File

@ -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

View File

@ -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 />,
},
],
},
{

View File

@ -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

View File

@ -21,5 +21,5 @@ export const StartPage = () => {
);
}
return <Navigate to={"/home"} replace />;
return <Navigate to={"/quotes"} replace />;
};

View File

@ -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>
);

View File

@ -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>
);

View File

@ -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,

View File

@ -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>

View 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>
);
};

View File

@ -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,
},
});
},
}),

View File

@ -1,2 +1,3 @@
export * from "./create";
export * from "./edit";
export * from "./list";

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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,

View File

@ -25,7 +25,7 @@ export const createAxiosAuthActions = (
return {
success: true,
data,
redirectTo: "/home",
redirectTo: "/",
};
} catch (error) {
return {

View File

@ -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);

View File

@ -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);

View File

@ -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",

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,3 @@
export * from "./Dealer";
export * from "./DealerRole";
export * from "./DealerStatus";

View File

@ -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;
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -0,0 +1,5 @@
export * from "./Quote";
export * from "./QuoteCustomer";
export * from "./QuoteItem";
export * from "./QuoteReference";
export * from "./QuoteStatus";

View File

@ -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";

View File

@ -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);

View File

@ -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,

View File

@ -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,

View File

@ -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);
};

View File

@ -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";

View File

@ -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);
};

View File

@ -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);
};

View File

@ -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;
};

View File

@ -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: [

View File

@ -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));
}
}

View File

@ -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";

View File

@ -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;