Compare commits
2 Commits
e6001d62a9
...
b4a6ac18f7
| Author | SHA1 | Date | |
|---|---|---|---|
| b4a6ac18f7 | |||
| 08f01e2b3b |
@ -2,6 +2,7 @@ import { Container, FormTextField } from "@/components";
|
|||||||
import { FormSubmitButton } from "@/components/Forms/FormSubmitButton";
|
import { FormSubmitButton } from "@/components/Forms/FormSubmitButton";
|
||||||
import { UeckoLogo } from "@/components/UeckoLogo/UeckoLogo";
|
import { UeckoLogo } from "@/components/UeckoLogo/UeckoLogo";
|
||||||
import { useLogin } from "@/lib/hooks";
|
import { useLogin } from "@/lib/hooks";
|
||||||
|
import { documentedLanguages } from "@/lib/languages";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
AlertDescription,
|
AlertDescription,
|
||||||
@ -29,17 +30,8 @@ import { SubmitHandler, useForm } from "react-hook-form";
|
|||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import SpanishJoiMessages from "../../spanish-joi-messages.json";
|
import SpanishJoiMessages from "../../spanish-joi-messages.json";
|
||||||
import "/node_modules/flag-icons/css/flag-icons.min.css";
|
|
||||||
|
|
||||||
type LoginDataForm = ILogin_DTO;
|
type LoginDataForm = ILogin_DTO;
|
||||||
|
|
||||||
const languages = [
|
|
||||||
{ code: "en", name: "English", flag: () => <span className='fi fi-gb'></span> },
|
|
||||||
{ code: "es", name: "Español", flag: () => <span className='fi fi-es'></span> },
|
|
||||||
//{ code: "fr", name: "Français", flag: () => <span className='fi fi-fr'></span> },
|
|
||||||
//{ code: "it", name: "Italiano", flag: () => <span className='fi fi-it'></span> },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const LoginPageWithLanguageSelector = () => {
|
export const LoginPageWithLanguageSelector = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
@ -49,7 +41,7 @@ export const LoginPageWithLanguageSelector = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const browserLang = navigator.language.split("-")[0];
|
const browserLang = navigator.language.split("-")[0];
|
||||||
const supportedLang = languages.find((lang) => lang.code === browserLang);
|
const supportedLang = documentedLanguages.find((lang) => lang.code === browserLang);
|
||||||
if (supportedLang) {
|
if (supportedLang) {
|
||||||
changeLanguage(supportedLang.code);
|
changeLanguage(supportedLang.code);
|
||||||
}
|
}
|
||||||
@ -113,16 +105,19 @@ export const LoginPageWithLanguageSelector = () => {
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant='outline' className='w-[140px] justify-start'>
|
<Button variant='outline' className='w-[140px] justify-start'>
|
||||||
<span className='mr-2'>
|
<span className='mr-2'>
|
||||||
{languages.find((lang) => lang.code === language)?.flag()}
|
<span
|
||||||
|
aria-hidden='true'
|
||||||
|
className={documentedLanguages.find((lang) => lang.code === language)?.flagClassName}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
{languages.find((lang) => lang.code === language)?.name}
|
{documentedLanguages.find((lang) => lang.code === language)?.name}
|
||||||
<ChevronDownIcon className='w-4 h-4 ml-auto opacity-50' />
|
<ChevronDownIcon className='w-4 h-4 ml-auto opacity-50' />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className='w-[140px]'>
|
<DropdownMenuContent className='w-[140px]'>
|
||||||
{languages.map((lang) => (
|
{documentedLanguages.map((lang) => (
|
||||||
<DropdownMenuItem key={lang.code} onSelect={() => changeLanguage(lang.code)}>
|
<DropdownMenuItem key={lang.code} onSelect={() => changeLanguage(lang.code)}>
|
||||||
<span className='mr-2'>{lang.flag()}</span>
|
<span aria-hidden='true' className={`mr-2 ${lang.flagClassName}`} />
|
||||||
{lang.name}
|
{lang.name}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -7,8 +7,6 @@ import { useMemo } from "react";
|
|||||||
import { useFormContext, useWatch } from "react-hook-form";
|
import { useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
const calculateQuoteTotals = (prices: any) => {
|
const calculateQuoteTotals = (prices: any) => {
|
||||||
console.debug("calculateQuoteTotals", prices);
|
|
||||||
|
|
||||||
const discountOrError = Percentage.create(
|
const discountOrError = Percentage.create(
|
||||||
prices[1] || {
|
prices[1] || {
|
||||||
amount: null,
|
amount: null,
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { FormDatePickerField, FormGroup, FormTextAreaField, FormTextField } from "@/components";
|
import { FormDatePickerField, FormGroup, FormTextAreaField, FormTextField } from "@/components";
|
||||||
import { Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui";
|
import { Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui";
|
||||||
|
import { documentedLanguages } from "@/lib/languages";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
export const QuoteGeneralCardEditor = () => {
|
export const QuoteGeneralCardEditor = () => {
|
||||||
const { formState } = useFormContext();
|
const { formState, control } = useFormContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='grid gap-6 md:grid-cols-6'>
|
<div className='grid gap-6 md:grid-cols-6'>
|
||||||
@ -12,6 +13,35 @@ export const QuoteGeneralCardEditor = () => {
|
|||||||
className='md:col-span-6'
|
className='md:col-span-6'
|
||||||
title={t("quotes.create.form_groups.general.title")}
|
title={t("quotes.create.form_groups.general.title")}
|
||||||
description={t("quotes.create.form_groups.general.desc")}
|
description={t("quotes.create.form_groups.general.desc")}
|
||||||
|
actions={
|
||||||
|
<div className='grid min-w-48 gap-1'>
|
||||||
|
<Label htmlFor='lang_code' className='text-xs text-muted-foreground'>
|
||||||
|
{t("quotes.form_fields.lang_code.label")}
|
||||||
|
</Label>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name='lang_code'
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select value={field.value || "es"} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger
|
||||||
|
id='lang_code'
|
||||||
|
aria-label={t("quotes.form_fields.lang_code.label")}
|
||||||
|
>
|
||||||
|
<SelectValue placeholder={t("quotes.form_fields.lang_code.placeholder")} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{documentedLanguages.map((language) => (
|
||||||
|
<SelectItem key={language.code} value={language.code}>
|
||||||
|
<span aria-hidden='true' className={`mr-2 ${language.flagClassName}`} />
|
||||||
|
{language.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className='grid grid-cols-6 gap-6'>
|
<div className='grid grid-cols-6 gap-6'>
|
||||||
<FormTextField
|
<FormTextField
|
||||||
@ -89,14 +119,6 @@ export const QuoteGeneralCardEditor = () => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<FormTextField
|
|
||||||
required
|
|
||||||
label={t("quotes.form_fields.lang_code.label")}
|
|
||||||
description={t("quotes.form_fields.lang_code.desc")}
|
|
||||||
//disabled={formState.disabled}
|
|
||||||
placeholder={t("quotes.form_fields.lang_code.placeholder")}
|
|
||||||
name='lang_code'
|
|
||||||
/>
|
|
||||||
<FormTextField
|
<FormTextField
|
||||||
required
|
required
|
||||||
label={t("quotes.form_fields.currency_code.label")}
|
label={t("quotes.form_fields.currency_code.label")}
|
||||||
|
|||||||
@ -10,13 +10,25 @@ import { t } from "i18next";
|
|||||||
|
|
||||||
import { SubmitButton } from "@/components";
|
import { SubmitButton } from "@/components";
|
||||||
import { useUnsavedChangesNotifier } from "@/lib/hooks";
|
import { useUnsavedChangesNotifier } from "@/lib/hooks";
|
||||||
import { Button, Form, Separator } from "@/ui";
|
import { documentedLanguages } from "@/lib/languages";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Label,
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
Separator,
|
||||||
|
} from "@/ui";
|
||||||
import { useToast } from "@/ui/use-toast";
|
import { useToast } from "@/ui/use-toast";
|
||||||
import { joiResolver } from "@hookform/resolvers/joi";
|
import { joiResolver } from "@hookform/resolvers/joi";
|
||||||
import { ICreateQuote_Request_DTO } from "@shared/contexts";
|
import { ICreateQuote_Request_DTO } from "@shared/contexts";
|
||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import SpanishJoiMessages from "../../spanish-joi-messages.json";
|
import SpanishJoiMessages from "../../spanish-joi-messages.json";
|
||||||
import { useQuotes } from "./hooks";
|
import { useQuotes } from "./hooks";
|
||||||
@ -25,18 +37,26 @@ interface QuoteDataForm extends ICreateQuote_Request_DTO {}
|
|||||||
|
|
||||||
export const QuoteCreate = () => {
|
export const QuoteCreate = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { i18n } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { useCreate } = useQuotes();
|
const { useCreate } = useQuotes();
|
||||||
const { mutate, isPending } = useCreate();
|
const { mutateAsync, isPending } = useCreate();
|
||||||
|
const [createdQuoteId, setCreatedQuoteId] = useState<string>();
|
||||||
|
|
||||||
|
const interfaceLanguage = i18n.resolvedLanguage ?? i18n.language;
|
||||||
|
const documentLanguage = documentedLanguages.some(({ code }) => code === interfaceLanguage)
|
||||||
|
? interfaceLanguage
|
||||||
|
: "es";
|
||||||
|
|
||||||
const defaultValues = useMemo(
|
const defaultValues = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
date: new Date(Date.now()).toUTCString(),
|
date: new Date(Date.now()).toUTCString(),
|
||||||
|
lang_code: documentLanguage,
|
||||||
customer_reference: "",
|
customer_reference: "",
|
||||||
customer_information: "",
|
customer_information: "",
|
||||||
//reference: "",
|
//reference: "",
|
||||||
}),
|
}),
|
||||||
[]
|
[documentLanguage]
|
||||||
);
|
);
|
||||||
|
|
||||||
const form = useForm<QuoteDataForm>({
|
const form = useForm<QuoteDataForm>({
|
||||||
@ -47,6 +67,9 @@ export const QuoteCreate = () => {
|
|||||||
customer_reference: Joi.string(),
|
customer_reference: Joi.string(),
|
||||||
date: Joi.date().required(),
|
date: Joi.date().required(),
|
||||||
customer_information: Joi.string().required(),
|
customer_information: Joi.string().required(),
|
||||||
|
lang_code: Joi.string()
|
||||||
|
.valid(...documentedLanguages.map(({ code }) => code))
|
||||||
|
.required(),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
messages: {
|
messages: {
|
||||||
@ -56,35 +79,35 @@ export const QuoteCreate = () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { formState, reset, getValues, handleSubmit } = form;
|
const { formState, reset, handleSubmit } = form;
|
||||||
const { isSubmitting, isDirty } = formState;
|
const { isSubmitting, isDirty } = formState;
|
||||||
|
|
||||||
useUnsavedChangesNotifier({
|
useUnsavedChangesNotifier({
|
||||||
isDirty,
|
isDirty,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (createdQuoteId) {
|
||||||
|
navigate(`/quotes/edit/${createdQuoteId}`, { relative: "path" });
|
||||||
|
}
|
||||||
|
}, [createdQuoteId, navigate]);
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<QuoteDataForm> = async (formData) => {
|
const onSubmit: SubmitHandler<QuoteDataForm> = async (formData) => {
|
||||||
try {
|
try {
|
||||||
mutate(formData, {
|
const data = await mutateAsync(formData);
|
||||||
onError: (error) => {
|
reset(formData);
|
||||||
console.debug(error);
|
|
||||||
toast({
|
|
||||||
title: t("common.error"),
|
|
||||||
description: error.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSuccess: (data) => {
|
|
||||||
reset(getValues());
|
|
||||||
toast({
|
toast({
|
||||||
title: t("quotes.notifications.created"),
|
title: t("quotes.notifications.created"),
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
navigate(`/quotes/edit/${data.id}`, { relative: "path" });
|
setCreatedQuoteId(data.id);
|
||||||
},
|
} catch (error) {
|
||||||
|
console.debug(error);
|
||||||
|
toast({
|
||||||
|
title: t("common.error"),
|
||||||
|
description: (error as Error).message,
|
||||||
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
//setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -118,7 +141,12 @@ export const QuoteCreate = () => {
|
|||||||
description={t("quotes.create.form_groups.general.desc")}
|
description={t("quotes.create.form_groups.general.desc")}
|
||||||
footerActions={
|
footerActions={
|
||||||
<div className='flex items-stretch justify-between flex-1'>
|
<div className='flex items-stretch justify-between flex-1'>
|
||||||
<Button size='sm' variant={"outline"} onClick={() => navigate("/quotes")}>
|
<Button
|
||||||
|
type='button'
|
||||||
|
size='sm'
|
||||||
|
variant={"outline"}
|
||||||
|
onClick={() => navigate("/quotes")}
|
||||||
|
>
|
||||||
{t("common.discard")}
|
{t("common.discard")}
|
||||||
</Button>
|
</Button>
|
||||||
<SubmitButton size='sm' label={t("common.continue")}></SubmitButton>
|
<SubmitButton size='sm' label={t("common.continue")}></SubmitButton>
|
||||||
@ -141,6 +169,37 @@ export const QuoteCreate = () => {
|
|||||||
name='date'
|
name='date'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className='grid gap-2'>
|
||||||
|
<Label htmlFor='lang_code' className='font-semibold'>
|
||||||
|
{t("quotes.form_fields.lang_code.label")}
|
||||||
|
</Label>
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name='lang_code'
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select value={field.value || "es"} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger
|
||||||
|
id='lang_code'
|
||||||
|
aria-label={t("quotes.form_fields.lang_code.label")}
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{documentedLanguages.map((language) => (
|
||||||
|
<SelectItem key={language.code} value={language.code}>
|
||||||
|
<span aria-hidden='true' className={`mr-2 ${language.flagClassName}`} />
|
||||||
|
{language.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<p className='text-sm text-muted-foreground'>
|
||||||
|
{t("quotes.form_fields.lang_code.desc")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<FormTextAreaField
|
<FormTextAreaField
|
||||||
|
|||||||
7
client/src/lib/languages.tsx
Normal file
7
client/src/lib/languages.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import "/node_modules/flag-icons/css/flag-icons.min.css";
|
||||||
|
|
||||||
|
/** Presentation-only options shared by selectors for supported document languages. */
|
||||||
|
export const documentedLanguages = [
|
||||||
|
{ code: "es", name: "Español", flagClassName: "fi fi-es" },
|
||||||
|
{ code: "en", name: "English", flagClassName: "fi fi-gb" },
|
||||||
|
] as const;
|
||||||
@ -339,9 +339,10 @@
|
|||||||
"placeholder": ""
|
"placeholder": ""
|
||||||
},
|
},
|
||||||
"lang_code": {
|
"lang_code": {
|
||||||
"label": "Language",
|
"label": "Document language",
|
||||||
"desc": "Quote language",
|
"desc": "Language used to generate the quote PDF",
|
||||||
"placeholder": ""
|
"placeholder": "",
|
||||||
|
"options": { "es": "Spanish", "en": "English" }
|
||||||
},
|
},
|
||||||
"currency_code": {
|
"currency_code": {
|
||||||
"label": "Currency",
|
"label": "Currency",
|
||||||
|
|||||||
@ -339,9 +339,10 @@
|
|||||||
"placeholder": ""
|
"placeholder": ""
|
||||||
},
|
},
|
||||||
"lang_code": {
|
"lang_code": {
|
||||||
"label": "Idioma",
|
"label": "Idioma del documento",
|
||||||
"desc": "Idioma de la cotización",
|
"desc": "Idioma utilizado para generar el presupuesto en PDF",
|
||||||
"placeholder": ""
|
"placeholder": "",
|
||||||
|
"options": { "es": "Español", "en": "Inglés" }
|
||||||
},
|
},
|
||||||
"currency_code": {
|
"currency_code": {
|
||||||
"label": "Moneda",
|
"label": "Moneda",
|
||||||
|
|||||||
@ -150,7 +150,7 @@ export class CreateQuoteUseCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
const languageOrError = Language.createFromCode(
|
const languageOrError = Language.createFromCode(
|
||||||
quoteDTO.lang_code ?? this._dealer?.language.code
|
quoteDTO.lang_code || Language.DEFAULT_LANGUAGE_CODE
|
||||||
);
|
);
|
||||||
if (languageOrError.isFailure) {
|
if (languageOrError.isFailure) {
|
||||||
return Result.fail(languageOrError.error);
|
return Result.fail(languageOrError.error);
|
||||||
|
|||||||
@ -165,7 +165,9 @@ export class UpdateQuoteUseCase
|
|||||||
return Result.fail(referenceOrError.error);
|
return Result.fail(referenceOrError.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const languageOrError = Language.createFromCode(quoteDTO.lang_code);
|
const languageOrError = Language.createFromCode(
|
||||||
|
quoteDTO.lang_code || Language.DEFAULT_LANGUAGE_CODE
|
||||||
|
);
|
||||||
if (languageOrError.isFailure) {
|
if (languageOrError.isFailure) {
|
||||||
return Result.fail(languageOrError.error);
|
return Result.fail(languageOrError.error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,7 +50,11 @@ class QuoteMapper
|
|||||||
date: this.mapsValue(source, "date", UTCDateValue.create),
|
date: this.mapsValue(source, "date", UTCDateValue.create),
|
||||||
reference: this.mapsValue(source, "reference", QuoteReference.create),
|
reference: this.mapsValue(source, "reference", QuoteReference.create),
|
||||||
currency: this.mapsValue(source, "currency_code", CurrencyData.createFromCode),
|
currency: this.mapsValue(source, "currency_code", CurrencyData.createFromCode),
|
||||||
language: this.mapsValue(source, "lang_code", Language.createFromCode),
|
language: this.mapsValue(
|
||||||
|
source,
|
||||||
|
"lang_code",
|
||||||
|
(value) => Language.createFromCode(value || Language.DEFAULT_LANGUAGE_CODE)
|
||||||
|
),
|
||||||
customerReference: this.mapsValue(
|
customerReference: this.mapsValue(
|
||||||
source,
|
source,
|
||||||
"customer_reference",
|
"customer_reference",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user