.
This commit is contained in:
parent
313659689e
commit
fe34458899
@ -1,22 +1,24 @@
|
|||||||
import { Button, ButtonProps } from "@/ui";
|
import { Button, ButtonProps } from "@/ui";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { PackagePlusIcon } from "lucide-react";
|
import { PackagePlusIcon } from "lucide-react";
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
|
||||||
export interface AppendCatalogArticleRowButtonProps extends ButtonProps {
|
export interface AppendCatalogArticleRowButtonProps extends ButtonProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppendCatalogArticleRowButton = ({
|
export const AppendCatalogArticleRowButton = forwardRef(
|
||||||
label = t("common.append_article"),
|
(
|
||||||
className,
|
{ label = t("common.append_article"), className, ...props }: AppendCatalogArticleRowButtonProps,
|
||||||
...props
|
ref
|
||||||
}: AppendCatalogArticleRowButtonProps): JSX.Element => (
|
): JSX.Element => (
|
||||||
<Button type='button' variant='outline' {...props}>
|
<Button type='button' variant='outline' {...props}>
|
||||||
{" "}
|
{" "}
|
||||||
<PackagePlusIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
|
<PackagePlusIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
|
||||||
{label && <>{label}</>}
|
{label && <>{label}</>}
|
||||||
</Button>
|
</Button>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
AppendCatalogArticleRowButton.displayName = "AddNewRowButton";
|
AppendCatalogArticleRowButton.displayName = "AddNewRowButton";
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { CreditCard, DownloadIcon, FilePenLineIcon, MoreVertical } from "lucide-react";
|
import { DownloadIcon, FilePenLineIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { ColorBadge } from "@/components";
|
||||||
|
import { useCustomLocalization } from "@/lib/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -9,10 +11,6 @@ import {
|
|||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
Separator,
|
Separator,
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
@ -23,8 +21,9 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/ui";
|
} from "@/ui";
|
||||||
import { useToast } from "@/ui/use-toast";
|
import { useToast } from "@/ui/use-toast";
|
||||||
|
import { CurrencyData } from "@shared/contexts";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useQuotes } from "../hooks";
|
import { useQuotes } from "../hooks";
|
||||||
import { DownloadQuoteDialog } from "./DownloadQuoteDialog";
|
import { DownloadQuoteDialog } from "./DownloadQuoteDialog";
|
||||||
@ -44,6 +43,33 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
|
|||||||
const { mutate: setStatusMutation } = useSetStatus(quoteId);
|
const { mutate: setStatusMutation } = useSetStatus(quoteId);
|
||||||
const { download, ...downloadProps } = useDownloader();
|
const { download, ...downloadProps } = useDownloader();
|
||||||
|
|
||||||
|
const { formatCurrency } = useCustomLocalization({
|
||||||
|
locale: data?.lang_code || "ES",
|
||||||
|
});
|
||||||
|
|
||||||
|
const currency_symbol = useMemo(() => {
|
||||||
|
const currencyOrError = data
|
||||||
|
? CurrencyData.createFromCode(data?.currency_code)
|
||||||
|
: CurrencyData.createDefaultCode();
|
||||||
|
return currencyOrError.isSuccess ? currencyOrError.object.symbol : "";
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const totals = useMemo(() => {
|
||||||
|
return data
|
||||||
|
? {
|
||||||
|
subtotal_price: formatCurrency(data.subtotal_price),
|
||||||
|
discount_price: formatCurrency(data.discount_price),
|
||||||
|
tax_price: formatCurrency(data.tax_price),
|
||||||
|
total_price: formatCurrency(data.total_price),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
subtotal_price: "0,00",
|
||||||
|
discount_price: "0,00",
|
||||||
|
tax_price: "0,00",
|
||||||
|
total_price: "0,00",
|
||||||
|
};
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
const handleOnChangeStatus = (_: string, newStatus: string) => {
|
const handleOnChangeStatus = (_: string, newStatus: string) => {
|
||||||
setStatusMutation(
|
setStatusMutation(
|
||||||
{ newStatus },
|
{ newStatus },
|
||||||
@ -90,183 +116,150 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
|
|||||||
<>
|
<>
|
||||||
<DownloadQuoteDialog {...downloadProps} onFinishDownload={handleFinishDownload} />
|
<DownloadQuoteDialog {...downloadProps} onFinishDownload={handleFinishDownload} />
|
||||||
<Tabs defaultValue='resume'>
|
<Tabs defaultValue='resume'>
|
||||||
<Card className='w-full overflow-hidden'>
|
<Card className='w-[390px] overflow-hidden'>
|
||||||
<CardHeader className='gap-3 border-b bg-accent'>
|
<CardHeader className='gap-3 border-b bg-accent'>
|
||||||
<CardTitle className='text-lg'>
|
<CardTitle className='flex items-center justify-between text-lg'>
|
||||||
{`${t("quotes.list.preview.quote")} #${data.reference}`}
|
<span>{t("quotes.list.resume.title")}</span>
|
||||||
|
<ColorBadge className='text-sm' label={t(`quotes.status.${data.status}`)} />
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className='flex items-center gap-1 mr-auto text-foreground'>
|
<CardDescription className='flex mr-auto text-foreground'>
|
||||||
<QuoteStatusEditor quote={data} onChangeStatus={handleOnChangeStatus} />
|
<div className='flex items-center gap-1'>
|
||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='h-8 gap-1'
|
className='h-8 gap-1'
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate(`/quotes/edit/${data.id}`, { relative: "path" });
|
navigate(`/quotes/edit/${data.id}`, { relative: "path" });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FilePenLineIcon className='h-3.5 w-3.5' />
|
<FilePenLineIcon className='h-3.5 w-3.5' />
|
||||||
<span className='sr-only md:not-sr-only md:whitespace-nowrap'>
|
<span className='sr-only md:not-sr-only md:whitespace-nowrap'>
|
||||||
{t("quotes.list.columns.actions.edit")}
|
{t("quotes.list.columns.actions.edit")}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
<QuoteStatusEditor quote={data} onChangeStatus={handleOnChangeStatus} />
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='h-8 gap-1'
|
className='h-8 gap-1'
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
>
|
>
|
||||||
<DownloadIcon className='h-3.5 w-3.5 ' />
|
<DownloadIcon className='h-3.5 w-3.5 ' />
|
||||||
<span className='sr-only'>{t("quotes.list.preview.download_quote")}</span>
|
<span className='sr-only'>{t("quotes.list.resume.download_quote")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{t("quotes.list.preview.download_quote")}</TooltipContent>
|
<TooltipContent>{t("quotes.list.resume.download_quote")}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger>
|
{/*<DropdownMenu>
|
||||||
<Tooltip>
|
<DropdownMenuTrigger>
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<Button size='icon' variant='outline' className='hidden w-8 h-8'>
|
<TooltipTrigger asChild>
|
||||||
<MoreVertical className='h-3.5 w-3.5' />
|
<Button size='icon' variant='outline' className='w-8 h-8'>
|
||||||
<span className='sr-only'>{t("common.more")}</span>
|
<MoreVertical className='h-3.5 w-3.5' />
|
||||||
</Button>
|
<span className='sr-only'>{t("common.more")}</span>
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipContent>{t("common.more")}</TooltipContent>
|
</TooltipTrigger>
|
||||||
</Tooltip>
|
<TooltipContent>{t("common.more")}</TooltipContent>
|
||||||
</DropdownMenuTrigger>
|
</Tooltip>
|
||||||
<DropdownMenuContent align='end'>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuItem className='not-sr-only 2xl:sr-only'>
|
<DropdownMenuContent align='end'>
|
||||||
<DownloadIcon className='h-3.5 w-3.5 mr-2' />
|
<DropdownMenuItem className='not-sr-only 2xl:sr-only'>
|
||||||
<span>{t("quotes.list.preview.download_quote")}</span>
|
<DownloadIcon className='h-3.5 w-3.5 mr-2' />
|
||||||
</DropdownMenuItem>
|
<span>{t("quotes.list.preview.download_quote")}</span>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuItem>
|
||||||
</DropdownMenu>
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>*/}
|
||||||
|
</div>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='p-6 text-sm'>
|
<CardContent className='p-6 text-sm'>
|
||||||
<TabsList className='grid w-full grid-cols-2'>
|
<TabsList className='grid w-full grid-cols-2'>
|
||||||
<TabsTrigger value='resume'>Resume</TabsTrigger>
|
<TabsTrigger value='resume'>{t("quotes.list.resume.tabs.resume")}</TabsTrigger>
|
||||||
<TabsTrigger value='preview'>Preview</TabsTrigger>
|
<TabsTrigger value='preview'>{t("quotes.list.resume.tabs.preview")}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value='resume' className='pt-4'>
|
<TabsContent value='resume' className='pt-4'>
|
||||||
<div className='grid gap-3'>
|
<div className='grid gap-3'>
|
||||||
<div className='grid gap-3'>
|
<div className='grid gap-3'>
|
||||||
<div className='font-semibold'>Quote information</div>
|
<div className='font-semibold'>{t("quotes.list.resume.quote_information")}</div>
|
||||||
|
|
||||||
<dl className='grid gap-3'>
|
<dl className='grid gap-3'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<dt className='text-muted-foreground'>
|
||||||
|
{t("quotes.form_fields.reference.label")}
|
||||||
|
</dt>
|
||||||
|
<dd className='font-medium'>{data.reference}</dd>
|
||||||
|
</div>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<dt className='text-muted-foreground'>
|
<dt className='text-muted-foreground'>
|
||||||
{t("quotes.form_fields.date.label")}
|
{t("quotes.form_fields.date.label")}
|
||||||
</dt>
|
</dt>
|
||||||
<dd>{new Date(data.date).toLocaleDateString()}</dd>
|
<dd className='font-medium'>{new Date(data.date).toLocaleDateString()}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-start justify-between'>
|
||||||
<dt className='text-muted-foreground'>Email</dt>
|
<dt className='text-muted-foreground whitespace-nowrap'>
|
||||||
<dd>
|
{t("quotes.form_fields.customer_reference.label")}
|
||||||
<a href='mailto:'>liam@acme.com</a>
|
</dt>
|
||||||
</dd>
|
<dd className='font-medium text-right whitespace-break-spaces'>
|
||||||
</div>
|
{data.customer_reference}
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<dt className='text-muted-foreground'>Phone</dt>
|
|
||||||
<dd>
|
|
||||||
<a href='tel:'>+1 234 567 890</a>
|
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<Separator className='my-4' />
|
<Separator className='my-4' />
|
||||||
<div className='grid gap-3'>
|
<div className='grid gap-3'>
|
||||||
<div className='font-semibold'>Customer Information</div>
|
<div className='font-semibold'>
|
||||||
Date: {new Date(data.date).toLocaleDateString()}
|
{t("quotes.list.resume.customer_information")}
|
||||||
|
</div>
|
||||||
<div>{data.customer_information}</div>
|
<div>{data.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>
|
</div>
|
||||||
<Separator className='my-4' />
|
<Separator className='my-4' />
|
||||||
<div className='font-semibold'>Order Details</div>
|
<div className='font-semibold'>{t("quotes.list.resume.price_information")}</div>
|
||||||
<ul className='grid gap-3'>
|
<ul className='grid gap-3'>
|
||||||
<li className='flex items-center justify-between'>
|
<li className='flex items-center justify-between'>
|
||||||
<span className='text-muted-foreground'>
|
<span className='text-muted-foreground'>
|
||||||
Glimmer Lamps x <span>2</span>
|
{t("quotes.form_fields.subtotal_price.label")}
|
||||||
</span>
|
</span>
|
||||||
<span>$250.00</span>
|
<span>{totals.subtotal_price}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className='flex items-center justify-between'>
|
<li className='flex items-center justify-between'>
|
||||||
<span className='text-muted-foreground'>
|
<span className='text-muted-foreground'>
|
||||||
Aqua Filters x <span>1</span>
|
{t("quotes.form_fields.discount.label")}
|
||||||
</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>
|
<span>$5.00</span>
|
||||||
</li>
|
</li>
|
||||||
<li className='flex items-center justify-between'>
|
<li className='flex items-center justify-between'>
|
||||||
<span className='text-muted-foreground'>Tax</span>
|
<span className='text-muted-foreground'>
|
||||||
|
{t("quotes.form_fields.discount_price.label")}
|
||||||
|
</span>
|
||||||
<span>$25.00</span>
|
<span>$25.00</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>
|
||||||
|
{t("quotes.form_fields.tax.label")}
|
||||||
|
</span>
|
||||||
|
<span>$5.00</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>
|
||||||
|
{t("quotes.form_fields.tax_price.label")}
|
||||||
|
</span>
|
||||||
|
<span>$25.00</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li className='flex items-center justify-between font-semibold'>
|
<li className='flex items-center justify-between font-semibold'>
|
||||||
<span className='text-muted-foreground'>Total</span>
|
<span className='text-muted-foreground'>
|
||||||
|
{t("quotes.form_fields.total_price.label")}
|
||||||
|
</span>
|
||||||
<span>$329.00</span>
|
<span>$329.00</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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'>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>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value='preview'></TabsContent>
|
<TabsContent value='preview'></TabsContent>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -88,7 +88,9 @@ export const QuotesDataTable = ({
|
|||||||
accessorKey: "status",
|
accessorKey: "status",
|
||||||
header: () => <>{t("quotes.list.columns.status")}</>,
|
header: () => <>{t("quotes.list.columns.status")}</>,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
cell: ({ row: { original } }) => <ColorBadge label={original.status} />,
|
cell: ({ row: { original } }) => (
|
||||||
|
<ColorBadge label={t(`quotes.status.${original.status}`)} />
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "date" as const,
|
id: "date" as const,
|
||||||
@ -282,7 +284,7 @@ export const QuotesDataTable = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{preview && (
|
{preview && (
|
||||||
<div id={previewId} className='flex items-stretch'>
|
<div id={previewId} className='flex items-stretch '>
|
||||||
<QuoteResume quoteId={activeRow?.original.id} />
|
<QuoteResume quoteId={activeRow?.original.id} />
|
||||||
{/*<QuotePDFPreview quote={activeRow?.original} className='flex-1' />*/}
|
{/*<QuotePDFPreview quote={activeRow?.original} className='flex-1' />*/}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -25,8 +25,8 @@ const quoteStatusTransitions: Record<TQuoteStatus, TQuoteStatus[]> = {
|
|||||||
draft: ["draft", "ready", "archived"],
|
draft: ["draft", "ready", "archived"],
|
||||||
ready: ["ready", "delivered", "archived"],
|
ready: ["ready", "delivered", "archived"],
|
||||||
delivered: ["delivered", "accepted", "rejected", "archived"],
|
delivered: ["delivered", "accepted", "rejected", "archived"],
|
||||||
accepted: ["accepted", "archived"],
|
accepted: ["accepted", "rejected", "archived"],
|
||||||
rejected: ["rejected", "archived"],
|
rejected: ["rejected", "accepted", "archived"],
|
||||||
archived: ["archived", "draft", "ready", "delivered", "accepted", "rejected"],
|
archived: ["archived", "draft", "ready", "delivered", "accepted", "rejected"],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,8 +53,6 @@ export const QuoteStatusEditor = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(newStatus);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|||||||
@ -1,23 +1,50 @@
|
|||||||
import { Badge } from "@/ui";
|
import { Badge } from "@/ui";
|
||||||
|
|
||||||
function stringToColor(str: string) {
|
function stringToColorPair(str: string) {
|
||||||
|
const TEXT_DARKEN_FACTOR = 0.7; // Factor para oscurecer el color del texto
|
||||||
|
const BACKGROUND_LIGHTEN_FACTOR = 0.7; // Factor para aclarar el color de fondo
|
||||||
|
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
let color = "#";
|
let color = "#";
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const value = (hash >> (i * 8)) & 0xff;
|
const value = (hash >> (i * 8)) & 0xff;
|
||||||
color += ("00" + value.toString(16)).substr(-2);
|
color += ("00" + value.toString(16)).substr(-2);
|
||||||
}
|
}
|
||||||
return color;
|
|
||||||
|
// Convert the color from hex to RGB
|
||||||
|
const r = parseInt(color.substr(1, 2), 16);
|
||||||
|
const g = parseInt(color.substr(3, 2), 16);
|
||||||
|
const b = parseInt(color.substr(5, 2), 16);
|
||||||
|
|
||||||
|
// Generate a darker shade for the text color
|
||||||
|
const textColor = `#${((r * TEXT_DARKEN_FACTOR) | 0).toString(16).padStart(2, "0")}${(
|
||||||
|
(g * TEXT_DARKEN_FACTOR) |
|
||||||
|
0
|
||||||
|
)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0")}${((b * TEXT_DARKEN_FACTOR) | 0).toString(16).padStart(2, "0")}`;
|
||||||
|
|
||||||
|
// Generate a much lighter shade for the background color
|
||||||
|
const bgColor = `#${Math.min(255, Math.floor(r + (255 - r) * BACKGROUND_LIGHTEN_FACTOR))
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0")}${Math.min(255, Math.floor(g + (255 - g) * BACKGROUND_LIGHTEN_FACTOR))
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0")}${Math.min(255, Math.floor(b + (255 - b) * BACKGROUND_LIGHTEN_FACTOR))
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
|
||||||
|
return [textColor, bgColor];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ColorBadge = ({ label, className }: { label: string; className?: string }) => {
|
export const ColorBadge = ({ label, className }: { label: string; className?: string }) => {
|
||||||
const backgroundColor = stringToColor(label);
|
const [color, backgroundColor] = stringToColorPair(label);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge className={className} style={{ backgroundColor, color: "#fff" }}>
|
<Badge className={className} style={{ backgroundColor, color }}>
|
||||||
{label}
|
{label}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,16 +1,12 @@
|
|||||||
/* https://github.com/mayank8aug/use-localization/blob/main/src/index.ts */
|
/* https://github.com/mayank8aug/use-localization/blob/main/src/index.ts */
|
||||||
|
|
||||||
import { IMoney, IPercentage, IQuantity } from "@/lib/types";
|
import { IMoney, IPercentage, IQuantity } from "@/lib/types";
|
||||||
|
import { adjustPrecision } from "@shared/utilities/helpers";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type UseLocalizationProps = {
|
type UseLocalizationProps = {
|
||||||
locale: string;
|
locale?: string;
|
||||||
};
|
|
||||||
|
|
||||||
const adjustPrecision = ({ amount, scale }: { amount: number; scale: number }) => {
|
|
||||||
const factor = 10 ** scale;
|
|
||||||
return Number(amount) / factor;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useLocalization = () => {
|
export const useLocalization = () => {
|
||||||
@ -32,10 +28,12 @@ export const useCustomLocalization = (props: UseLocalizationProps) => {
|
|||||||
|
|
||||||
const { amount, scale, currency_code } = value;
|
const { amount, scale, currency_code } = value;
|
||||||
|
|
||||||
return new Intl.NumberFormat(locale, {
|
return new Intl.NumberFormat(locale ?? "ES", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: currency_code,
|
currency: currency_code,
|
||||||
currencyDisplay: "symbol",
|
currencyDisplay: "symbol",
|
||||||
|
useGrouping: true,
|
||||||
|
|
||||||
maximumFractionDigits: scale,
|
maximumFractionDigits: scale,
|
||||||
}).format(amount === null ? 0 : amount);
|
}).format(amount === null ? 0 : amount);
|
||||||
},
|
},
|
||||||
@ -53,8 +51,8 @@ export const useCustomLocalization = (props: UseLocalizationProps) => {
|
|||||||
const result = new Intl.NumberFormat("es", {
|
const result = new Intl.NumberFormat("es", {
|
||||||
/*minimumSignificantDigits: scale,
|
/*minimumSignificantDigits: scale,
|
||||||
maximumSignificantDigits: scale,
|
maximumSignificantDigits: scale,
|
||||||
minimumFractionDigits: scale,
|
minimumFractionDigits: scale,*/
|
||||||
useGrouping: true,*/
|
useGrouping: true,
|
||||||
}).format(amount === null ? 0 : adjustPrecision({ amount, scale }));
|
}).format(amount === null ? 0 : adjustPrecision({ amount, scale }));
|
||||||
|
|
||||||
//console.log(value, result);
|
//console.log(value, result);
|
||||||
|
|||||||
@ -138,10 +138,19 @@
|
|||||||
"edit": "Edit quote"
|
"edit": "Edit quote"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resume": {},
|
|
||||||
"preview": {
|
"resume": {
|
||||||
"quote": "Quote",
|
"title": "Quote",
|
||||||
"download_quote": "Download quote"
|
"download_quote": "Download quote",
|
||||||
|
"tabs": {
|
||||||
|
"resume": "Resume",
|
||||||
|
"preview": "Preview"
|
||||||
|
},
|
||||||
|
|
||||||
|
"quote_information": "Quote Information",
|
||||||
|
"customer_information": "Customer Information",
|
||||||
|
"payment_information": "Payment Information",
|
||||||
|
"price_information": "Quote totals"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
@ -250,6 +259,11 @@
|
|||||||
"desc": "Quote reference",
|
"desc": "Quote reference",
|
||||||
"placeholder": ""
|
"placeholder": ""
|
||||||
},
|
},
|
||||||
|
"status": {
|
||||||
|
"label": "Status",
|
||||||
|
"desc": "Quote status",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
"lang_code": {
|
"lang_code": {
|
||||||
"label": "Language",
|
"label": "Language",
|
||||||
"desc": "Quote language",
|
"desc": "Quote language",
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import DineroFactory, { Currency, Dinero } from "dinero.js";
|
|||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import { isNull } from "lodash";
|
import { isNull } from "lodash";
|
||||||
import { NullOr } from "../../../../utilities";
|
import { NullOr } from "../../../../utilities";
|
||||||
|
import { adjustPrecision } from "../../../../utilities/helpers";
|
||||||
import { DomainError, handleDomainError } from "../errors";
|
import { DomainError, handleDomainError } from "../errors";
|
||||||
import { RuleValidator } from "../RuleValidator";
|
import { RuleValidator } from "../RuleValidator";
|
||||||
import { CurrencyData } from "./CurrencyData";
|
import { CurrencyData } from "./CurrencyData";
|
||||||
@ -254,6 +255,13 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
new Intl.NumberFormat("es", {
|
||||||
|
/*minimumSignificantDigits: scale,
|
||||||
|
maximumSignificantDigits: scale,
|
||||||
|
minimumFractionDigits: scale,*/
|
||||||
|
useGrouping: true,
|
||||||
|
}).format(value === null ? 0 : adjustPrecision({ amount: value, scale }));
|
||||||
|
|
||||||
const factor = Math.pow(10, scale);
|
const factor = Math.pow(10, scale);
|
||||||
const amount = Number(value) / factor;
|
const amount = Number(value) / factor;
|
||||||
return amount.toFixed(scale);
|
return amount.toFixed(scale);
|
||||||
|
|||||||
4
shared/lib/utilities/helpers.ts
Normal file
4
shared/lib/utilities/helpers.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const adjustPrecision = ({ amount, scale }: { amount: number; scale: number }) => {
|
||||||
|
const factor = 10 ** scale;
|
||||||
|
return Number(amount) / factor;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user