This commit is contained in:
David Arranz 2024-08-28 20:38:20 +02:00
parent 313659689e
commit fe34458899
9 changed files with 217 additions and 171 deletions

View File

@ -1,22 +1,24 @@
import { Button, ButtonProps } from "@/ui";
import { t } from "i18next";
import { PackagePlusIcon } from "lucide-react";
import { forwardRef } from "react";
export interface AppendCatalogArticleRowButtonProps extends ButtonProps {
label?: string;
className?: string;
}
export const AppendCatalogArticleRowButton = ({
label = t("common.append_article"),
className,
...props
}: AppendCatalogArticleRowButtonProps): JSX.Element => (
<Button type='button' variant='outline' {...props}>
{" "}
<PackagePlusIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
{label && <>{label}</>}
</Button>
export const AppendCatalogArticleRowButton = forwardRef(
(
{ label = t("common.append_article"), className, ...props }: AppendCatalogArticleRowButtonProps,
ref
): JSX.Element => (
<Button type='button' variant='outline' {...props}>
{" "}
<PackagePlusIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
{label && <>{label}</>}
</Button>
)
);
AppendCatalogArticleRowButton.displayName = "AddNewRowButton";

View File

@ -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 {
Button,
@ -9,10 +11,6 @@ import {
CardFooter,
CardHeader,
CardTitle,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Separator,
Tabs,
TabsContent,
@ -23,8 +21,9 @@ import {
TooltipTrigger,
} from "@/ui";
import { useToast } from "@/ui/use-toast";
import { CurrencyData } from "@shared/contexts";
import { t } from "i18next";
import { useCallback } from "react";
import { useCallback, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useQuotes } from "../hooks";
import { DownloadQuoteDialog } from "./DownloadQuoteDialog";
@ -44,6 +43,33 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
const { mutate: setStatusMutation } = useSetStatus(quoteId);
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) => {
setStatusMutation(
{ newStatus },
@ -90,183 +116,150 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
<>
<DownloadQuoteDialog {...downloadProps} onFinishDownload={handleFinishDownload} />
<Tabs defaultValue='resume'>
<Card className='w-full overflow-hidden'>
<Card className='w-[390px] overflow-hidden'>
<CardHeader className='gap-3 border-b bg-accent'>
<CardTitle className='text-lg'>
{`${t("quotes.list.preview.quote")} #${data.reference}`}
<CardTitle className='flex items-center justify-between text-lg'>
<span>{t("quotes.list.resume.title")}</span>
<ColorBadge className='text-sm' label={t(`quotes.status.${data.status}`)} />
</CardTitle>
<CardDescription className='flex items-center gap-1 mr-auto text-foreground'>
<QuoteStatusEditor quote={data} onChangeStatus={handleOnChangeStatus} />
<Button
size='sm'
variant='outline'
className='h-8 gap-1'
onClick={(e) => {
e.preventDefault();
navigate(`/quotes/edit/${data.id}`, { relative: "path" });
}}
>
<FilePenLineIcon className='h-3.5 w-3.5' />
<span className='sr-only md:not-sr-only md:whitespace-nowrap'>
{t("quotes.list.columns.actions.edit")}
</span>
</Button>
<CardDescription className='flex mr-auto text-foreground'>
<div className='flex items-center gap-1'>
<Button
size='sm'
variant='outline'
className='h-8 gap-1'
onClick={(e) => {
e.preventDefault();
navigate(`/quotes/edit/${data.id}`, { relative: "path" });
}}
>
<FilePenLineIcon className='h-3.5 w-3.5' />
<span className='sr-only md:not-sr-only md:whitespace-nowrap'>
{t("quotes.list.columns.actions.edit")}
</span>
</Button>
<QuoteStatusEditor quote={data} onChangeStatus={handleOnChangeStatus} />
<Tooltip>
<TooltipTrigger asChild>
<Button
size='sm'
variant='outline'
className='h-8 gap-1'
onClick={handleDownload}
>
<DownloadIcon className='h-3.5 w-3.5 ' />
<span className='sr-only'>{t("quotes.list.preview.download_quote")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("quotes.list.preview.download_quote")}</TooltipContent>
</Tooltip>
<DropdownMenu>
<DropdownMenuTrigger>
<Tooltip>
<TooltipTrigger asChild>
<Button size='icon' variant='outline' className='hidden w-8 h-8'>
<MoreVertical className='h-3.5 w-3.5' />
<span className='sr-only'>{t("common.more")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.more")}</TooltipContent>
</Tooltip>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem className='not-sr-only 2xl:sr-only'>
<DownloadIcon className='h-3.5 w-3.5 mr-2' />
<span>{t("quotes.list.preview.download_quote")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<Button
size='sm'
variant='outline'
className='h-8 gap-1'
onClick={handleDownload}
>
<DownloadIcon className='h-3.5 w-3.5 ' />
<span className='sr-only'>{t("quotes.list.resume.download_quote")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("quotes.list.resume.download_quote")}</TooltipContent>
</Tooltip>
{/*<DropdownMenu>
<DropdownMenuTrigger>
<Tooltip>
<TooltipTrigger asChild>
<Button size='icon' variant='outline' className='w-8 h-8'>
<MoreVertical className='h-3.5 w-3.5' />
<span className='sr-only'>{t("common.more")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.more")}</TooltipContent>
</Tooltip>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem className='not-sr-only 2xl:sr-only'>
<DownloadIcon className='h-3.5 w-3.5 mr-2' />
<span>{t("quotes.list.preview.download_quote")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>*/}
</div>
</CardDescription>
</CardHeader>
<CardContent className='p-6 text-sm'>
<TabsList className='grid w-full grid-cols-2'>
<TabsTrigger value='resume'>Resume</TabsTrigger>
<TabsTrigger value='preview'>Preview</TabsTrigger>
<TabsTrigger value='resume'>{t("quotes.list.resume.tabs.resume")}</TabsTrigger>
<TabsTrigger value='preview'>{t("quotes.list.resume.tabs.preview")}</TabsTrigger>
</TabsList>
<TabsContent value='resume' className='pt-4'>
<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'>
<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'>
<dt className='text-muted-foreground'>
{t("quotes.form_fields.date.label")}
</dt>
<dd>{new Date(data.date).toLocaleDateString()}</dd>
<dd className='font-medium'>{new Date(data.date).toLocaleDateString()}</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>
<div className='flex items-start justify-between'>
<dt className='text-muted-foreground whitespace-nowrap'>
{t("quotes.form_fields.customer_reference.label")}
</dt>
<dd className='font-medium text-right whitespace-break-spaces'>
{data.customer_reference}
</dd>
</div>
</dl>
</div>
<Separator className='my-4' />
<div className='grid gap-3'>
<div className='font-semibold'>Customer Information</div>
Date: {new Date(data.date).toLocaleDateString()}
<div className='font-semibold'>
{t("quotes.list.resume.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>
<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'>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>
Glimmer Lamps x <span>2</span>
{t("quotes.form_fields.subtotal_price.label")}
</span>
<span>$250.00</span>
<span>{totals.subtotal_price}</span>
</li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>
Aqua Filters x <span>1</span>
{t("quotes.form_fields.discount.label")}
</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 className='text-muted-foreground'>
{t("quotes.form_fields.discount_price.label")}
</span>
<span>$25.00</span>
</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'>
<span className='text-muted-foreground'>Total</span>
<span className='text-muted-foreground'>
{t("quotes.form_fields.total_price.label")}
</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'>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 value='preview'></TabsContent>
</CardContent>

View File

@ -88,7 +88,9 @@ export const QuotesDataTable = ({
accessorKey: "status",
header: () => <>{t("quotes.list.columns.status")}</>,
// 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,
@ -282,7 +284,7 @@ export const QuotesDataTable = ({
</div>
{preview && (
<div id={previewId} className='flex items-stretch'>
<div id={previewId} className='flex items-stretch '>
<QuoteResume quoteId={activeRow?.original.id} />
{/*<QuotePDFPreview quote={activeRow?.original} className='flex-1' />*/}
</div>

View File

@ -25,8 +25,8 @@ const quoteStatusTransitions: Record<TQuoteStatus, TQuoteStatus[]> = {
draft: ["draft", "ready", "archived"],
ready: ["ready", "delivered", "archived"],
delivered: ["delivered", "accepted", "rejected", "archived"],
accepted: ["accepted", "archived"],
rejected: ["rejected", "archived"],
accepted: ["accepted", "rejected", "archived"],
rejected: ["rejected", "accepted", "archived"],
archived: ["archived", "draft", "ready", "delivered", "accepted", "rejected"],
};
@ -53,8 +53,6 @@ export const QuoteStatusEditor = ({
}
};
console.log(newStatus);
return (
<Dialog>
<DialogTrigger asChild>

View File

@ -1,23 +1,50 @@
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;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
let color = "#";
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
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 }) => {
const backgroundColor = stringToColor(label);
const [color, backgroundColor] = stringToColorPair(label);
return (
<Badge className={className} style={{ backgroundColor, color: "#fff" }}>
<Badge className={className} style={{ backgroundColor, color }}>
{label}
</Badge>
);

View File

@ -1,16 +1,12 @@
/* https://github.com/mayank8aug/use-localization/blob/main/src/index.ts */
import { IMoney, IPercentage, IQuantity } from "@/lib/types";
import { adjustPrecision } from "@shared/utilities/helpers";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
type UseLocalizationProps = {
locale: string;
};
const adjustPrecision = ({ amount, scale }: { amount: number; scale: number }) => {
const factor = 10 ** scale;
return Number(amount) / factor;
locale?: string;
};
export const useLocalization = () => {
@ -32,10 +28,12 @@ export const useCustomLocalization = (props: UseLocalizationProps) => {
const { amount, scale, currency_code } = value;
return new Intl.NumberFormat(locale, {
return new Intl.NumberFormat(locale ?? "ES", {
style: "currency",
currency: currency_code,
currencyDisplay: "symbol",
useGrouping: true,
maximumFractionDigits: scale,
}).format(amount === null ? 0 : amount);
},
@ -53,8 +51,8 @@ export const useCustomLocalization = (props: UseLocalizationProps) => {
const result = new Intl.NumberFormat("es", {
/*minimumSignificantDigits: scale,
maximumSignificantDigits: scale,
minimumFractionDigits: scale,
useGrouping: true,*/
minimumFractionDigits: scale,*/
useGrouping: true,
}).format(amount === null ? 0 : adjustPrecision({ amount, scale }));
//console.log(value, result);

View File

@ -138,10 +138,19 @@
"edit": "Edit quote"
}
},
"resume": {},
"preview": {
"quote": "Quote",
"download_quote": "Download quote"
"resume": {
"title": "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": {
@ -250,6 +259,11 @@
"desc": "Quote reference",
"placeholder": ""
},
"status": {
"label": "Status",
"desc": "Quote status",
"placeholder": ""
},
"lang_code": {
"label": "Language",
"desc": "Quote language",

View File

@ -4,6 +4,7 @@ import DineroFactory, { Currency, Dinero } from "dinero.js";
import Joi from "joi";
import { isNull } from "lodash";
import { NullOr } from "../../../../utilities";
import { adjustPrecision } from "../../../../utilities/helpers";
import { DomainError, handleDomainError } from "../errors";
import { RuleValidator } from "../RuleValidator";
import { CurrencyData } from "./CurrencyData";
@ -254,6 +255,13 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
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 amount = Number(value) / factor;
return amount.toFixed(scale);

View File

@ -0,0 +1,4 @@
export const adjustPrecision = ({ amount, scale }: { amount: number; scale: number }) => {
const factor = 10 ** scale;
return Number(amount) / factor;
};