Clientes y facturas de cliente
This commit is contained in:
parent
11402bccc1
commit
9ef847d54b
@ -10,6 +10,12 @@
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wdth,wght@0,75..100,100..900;1,75..100,100..900&display=swap"
|
||||
rel="stylesheet">
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Domine:wght@400..700&family=Roboto:ital,wdth,wght@0,75..100,100..900;1,75..100,100..900&display=swap"
|
||||
rel="stylesheet">
|
||||
|
||||
<title>FactuGES 2025</title>
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
|
||||
@ -42,7 +42,6 @@ export const createAxiosInstance = ({
|
||||
}: AxiosFactoryConfig): AxiosInstance => {
|
||||
const instance = axios.create(defaultAxiosRequestConfig);
|
||||
instance.defaults.baseURL = baseURL;
|
||||
setupInterceptors(instance, getAccessToken, onAuthError);
|
||||
|
||||
return instance;
|
||||
return setupInterceptors(instance, getAccessToken, onAuthError);
|
||||
};
|
||||
|
||||
@ -3,16 +3,16 @@ import { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from "axios";
|
||||
/**
|
||||
* Configura interceptores para una instancia de Axios.
|
||||
*
|
||||
* @param instance - Instancia de Axios que será modificada.
|
||||
* @param axiosInstance - Instancia de Axios que será modificada.
|
||||
* @param getAccessToken - Función que devuelve el token JWT actual.
|
||||
* @param onAuthError - Función opcional que se ejecuta ante errores de autenticación (status 401).
|
||||
*/
|
||||
export const setupInterceptors = (
|
||||
instance: AxiosInstance,
|
||||
axiosInstance: AxiosInstance,
|
||||
getAccessToken: () => string | null,
|
||||
onAuthError?: () => void
|
||||
): void => {
|
||||
instance.interceptors.request.use(
|
||||
) => {
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const token = getAccessToken();
|
||||
if (token && config.headers) {
|
||||
@ -25,13 +25,48 @@ export const setupInterceptors = (
|
||||
}
|
||||
);
|
||||
|
||||
instance.interceptors.response.use(
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
if (error.response?.status === 401 && onAuthError) {
|
||||
// 🔴 Transformamos SIEMPRE el error antes de propagarlo
|
||||
const normalized = normalizeAxiosError(error);
|
||||
return Promise.reject(normalized);
|
||||
|
||||
/*if (error.response?.status === 401 && onAuthError) {
|
||||
onAuthError();
|
||||
}
|
||||
return Promise.reject(error);
|
||||
} */
|
||||
}
|
||||
);
|
||||
|
||||
return axiosInstance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normaliza errores de Axios en un objeto estándar de Error
|
||||
* con propiedades extra opcionales (status, raw).
|
||||
*/
|
||||
function normalizeAxiosError(error: AxiosError): Error {
|
||||
let normalizedError: Error;
|
||||
|
||||
if (error.response?.data) {
|
||||
const data: any = error.response.data;
|
||||
|
||||
// Intentamos localizar mensaje en campos comunes
|
||||
const msg =
|
||||
data.message ??
|
||||
(Array.isArray(data.errors) && data.errors[0]?.msg) ??
|
||||
error.message ??
|
||||
"Unknown server error";
|
||||
|
||||
normalizedError = new Error(msg);
|
||||
|
||||
// Añadimos metadatos útiles
|
||||
(normalizedError as any).status = error.response.status;
|
||||
(normalizedError as any).raw = data;
|
||||
} else {
|
||||
normalizedError = new Error(error.message || "Unknown network error");
|
||||
(normalizedError as any).status = error.response?.status ?? 0;
|
||||
}
|
||||
|
||||
return normalizedError;
|
||||
}
|
||||
|
||||
@ -3,5 +3,5 @@ import { UtcDate } from "@repo/rdx-ddd";
|
||||
export function formatDateDTO(dateString: string) {
|
||||
const result = UtcDate.createFromISO(dateString).data;
|
||||
|
||||
return result.toDateString();
|
||||
return result.toEuropeanString();
|
||||
}
|
||||
|
||||
@ -1,9 +1,18 @@
|
||||
import { MoneyDTO } from "@erp/core";
|
||||
import { MoneyValue } from "@repo/rdx-ddd";
|
||||
|
||||
export function formatMoneyDTO(amount: MoneyDTO, locale: string) {
|
||||
if (amount.value === "") {
|
||||
return "";
|
||||
export type FormatMoneyOptions = {
|
||||
locale: string;
|
||||
hideZeros?: boolean;
|
||||
newScale?: number;
|
||||
};
|
||||
|
||||
export function formatMoneyDTO(
|
||||
amount: MoneyDTO,
|
||||
{ locale, hideZeros = false, newScale = 2 }: FormatMoneyOptions
|
||||
) {
|
||||
if (hideZeros && (amount.value === "0" || amount.value === "")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const money = MoneyValue.create({
|
||||
@ -12,5 +21,5 @@ export function formatMoneyDTO(amount: MoneyDTO, locale: string) {
|
||||
scale: Number(amount.scale),
|
||||
}).data;
|
||||
|
||||
return money.format(locale);
|
||||
return money.convertScale(newScale).format(locale);
|
||||
}
|
||||
|
||||
@ -2,6 +2,10 @@ import { PercentageDTO } from "@erp/core";
|
||||
import { Percentage } from "@repo/rdx-ddd";
|
||||
|
||||
export function formatPercentageDTO(Percentage_value: PercentageDTO, locale: string) {
|
||||
if (Percentage_value.value === "0" || Percentage_value.value === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = Percentage.create({
|
||||
value: Number(Percentage_value.value),
|
||||
scale: Number(Percentage_value.scale),
|
||||
|
||||
@ -2,8 +2,8 @@ import { QuantityDTO } from "@erp/core";
|
||||
import { Quantity } from "@repo/rdx-ddd";
|
||||
|
||||
export function formatQuantityDTO(quantity_value: QuantityDTO) {
|
||||
if (quantity_value.value === "") {
|
||||
return "";
|
||||
if (quantity_value.value === "0" || quantity_value.value === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = Quantity.create({
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { IPresenterOutputParams, Presenter } from "@erp/core/api";
|
||||
import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
|
||||
import { ArrayElement } from "@repo/rdx-utils";
|
||||
import { formatMoneyDTO, formatQuantityDTO } from "../../helpers";
|
||||
import { FormatMoneyOptions, formatMoneyDTO, formatQuantityDTO } from "../../helpers";
|
||||
|
||||
type CustomerInvoiceItemsDTO = GetCustomerInvoiceByIdResponseDTO["items"];
|
||||
type CustomerInvoiceItemDTO = ArrayElement<CustomerInvoiceItemsDTO>;
|
||||
@ -13,18 +13,23 @@ export class CustomerInvoiceItemsReportPersenter extends Presenter<
|
||||
private _locale!: string;
|
||||
|
||||
private _mapItem(invoiceItem: CustomerInvoiceItemDTO, index: number) {
|
||||
const moneyOptions: FormatMoneyOptions = {
|
||||
locale: this._locale,
|
||||
hideZeros: true,
|
||||
newScale: 2,
|
||||
};
|
||||
|
||||
return {
|
||||
...invoiceItem,
|
||||
|
||||
quantity: formatQuantityDTO(invoiceItem.quantity),
|
||||
unit_amount: formatMoneyDTO(invoiceItem.unit_amount, this._locale),
|
||||
|
||||
subtotal_amount: formatMoneyDTO(invoiceItem.subtotal_amount, this._locale),
|
||||
unit_amount: formatMoneyDTO(invoiceItem.unit_amount, moneyOptions),
|
||||
subtotal_amount: formatMoneyDTO(invoiceItem.subtotal_amount, moneyOptions),
|
||||
// discount_percetage: formatPercentageDTO(invoiceItem.discount_percentage, this._locale),
|
||||
discount_amount: formatMoneyDTO(invoiceItem.discount_amount, this._locale),
|
||||
taxable_amount: formatMoneyDTO(invoiceItem.taxable_amount, this._locale),
|
||||
taxes_amount: formatMoneyDTO(invoiceItem.taxes_amount, this._locale),
|
||||
total_amount: formatMoneyDTO(invoiceItem.total_amount, this._locale),
|
||||
discount_amount: formatMoneyDTO(invoiceItem.discount_amount, moneyOptions),
|
||||
taxable_amount: formatMoneyDTO(invoiceItem.taxable_amount, moneyOptions),
|
||||
taxes_amount: formatMoneyDTO(invoiceItem.taxes_amount, moneyOptions),
|
||||
total_amount: formatMoneyDTO(invoiceItem.total_amount, moneyOptions),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { Presenter } from "@erp/core/api";
|
||||
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
|
||||
import { formatDateDTO, formatMoneyDTO, formatPercentageDTO } from "../../helpers";
|
||||
import {
|
||||
FormatMoneyOptions,
|
||||
formatDateDTO,
|
||||
formatMoneyDTO,
|
||||
formatPercentageDTO,
|
||||
} from "../../helpers";
|
||||
|
||||
export class CustomerInvoiceReportPresenter extends Presenter<
|
||||
GetCustomerInvoiceByIdResponseDTO,
|
||||
@ -18,17 +23,23 @@ export class CustomerInvoiceReportPresenter extends Presenter<
|
||||
locale,
|
||||
});
|
||||
|
||||
const moneyOptions: FormatMoneyOptions = {
|
||||
locale,
|
||||
hideZeros: true,
|
||||
newScale: 2,
|
||||
};
|
||||
|
||||
return {
|
||||
...invoiceDTO,
|
||||
items: itemsDTO,
|
||||
|
||||
invoice_date: formatDateDTO(invoiceDTO.invoice_date),
|
||||
subtotal_amount: formatMoneyDTO(invoiceDTO.subtotal_amount, locale),
|
||||
discount_percetage: formatPercentageDTO(invoiceDTO.discount_percentage, locale),
|
||||
discount_amount: formatMoneyDTO(invoiceDTO.discount_amount, locale),
|
||||
taxable_amount: formatMoneyDTO(invoiceDTO.taxable_amount, locale),
|
||||
taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, locale),
|
||||
total_amount: formatMoneyDTO(invoiceDTO.total_amount, locale),
|
||||
subtotal_amount: formatMoneyDTO(invoiceDTO.subtotal_amount, moneyOptions),
|
||||
discount_percentage: formatPercentageDTO(invoiceDTO.discount_percentage, locale),
|
||||
discount_amount: formatMoneyDTO(invoiceDTO.discount_amount, moneyOptions),
|
||||
taxable_amount: formatMoneyDTO(invoiceDTO.taxable_amount, moneyOptions),
|
||||
taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, moneyOptions),
|
||||
total_amount: formatMoneyDTO(invoiceDTO.total_amount, moneyOptions),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ export class CustomerInvoiceReportPDFPresenter extends Presenter<
|
||||
await page.setContent(htmlData, { waitUntil: "networkidle2" });
|
||||
|
||||
await navigationPromise;
|
||||
|
||||
const reportPDF = await report.pdfPage(page, {
|
||||
format: "A4",
|
||||
margin: {
|
||||
@ -48,6 +49,10 @@ export class CustomerInvoiceReportPDFPresenter extends Presenter<
|
||||
preferCSSPageSize: true,
|
||||
omitBackground: false,
|
||||
printBackground: true,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: "<div />",
|
||||
footerTemplate:
|
||||
'<div style="text-align: center;width: 297mm;font-size: 10px;">Página <span style="margin-right: 1cm"><span class="pageNumber"></span> de <span class="totalPages"></span></span></div>',
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
@ -55,14 +55,14 @@
|
||||
|
||||
table th,
|
||||
table td {
|
||||
border: 0px solid #ccc;
|
||||
border: 0px solid;
|
||||
padding: 3px 10px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: #f5f5f5;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.totals {
|
||||
@ -81,7 +81,7 @@
|
||||
|
||||
footer {
|
||||
margin-top: 40px;
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
@ -119,7 +119,7 @@
|
||||
<div class="p-1 ">
|
||||
<p>Factura nº:<strong> {{invoice_number}}</strong></p>
|
||||
<p><span>Fecha:<strong> {{invoice_date}}</strong></p>
|
||||
<p><span>Página:</span>1 / 1</p>
|
||||
<p><span>Página:</span><span class="pageNumber"></span> / <span class="totalPages"></span></p>
|
||||
</div>
|
||||
<div class="p-1 ml-9">
|
||||
<h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2>
|
||||
@ -174,9 +174,10 @@
|
||||
{{#each items}}
|
||||
<tr>
|
||||
<td>{{description}}</td>
|
||||
<td class="text-right">{{quantity}}</td>
|
||||
<td class="text-right">{{unit_amount}}</td>
|
||||
<td class="text-right">{{total_amount}}</td>
|
||||
<td class="text-right">{{#if quantity}}{{quantity}}{{else}} {{/if}}</td>
|
||||
<td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}} {{/if}}</td>
|
||||
<td class="text-right">{{#if total_amount}}{{total_amount}}{{else}} {{/if}}</td>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
@ -197,14 +198,14 @@
|
||||
<div class="relative pt-10 grow">
|
||||
<table class="table-header min-w-full bg-transparent">
|
||||
<tbody>
|
||||
{{#if percentage}}
|
||||
{{#if discount_percentage}}
|
||||
<tr>
|
||||
<td class="px-4 text-right">Importe neto</td>
|
||||
<td class="w-5"> </td>
|
||||
<td class="px-4 text-right">{{subtotal_amount}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 text-right">Descuento 0%</td>
|
||||
<td class="px-4 text-right">Descuento {{discount_percentage}}</td>
|
||||
<td class="w-5"> </td>
|
||||
<td class="px-4 text-right">{{discount_amount.value}}</td>
|
||||
</tr>
|
||||
@ -237,7 +238,8 @@
|
||||
|
||||
<footer id="footer" class="mt-4">
|
||||
<aside>
|
||||
<p>Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212 | CIF: B83999441 -
|
||||
<p class="text-center">Insc. en el Reg. Merc. de Madrid, Tomo 20.073, Libro 0, Folio 141, Sección 8, Hoja M-354212
|
||||
| CIF: B83999441 -
|
||||
Rodax Software S.L.</p>
|
||||
</aside>
|
||||
</footer>
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
import { ITransactionManager } from "@erp/core/api";
|
||||
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { UpdateCustomerRequestDTO } from "../../../common";
|
||||
import { CustomerPatchProps, CustomerService } from "../../domain";
|
||||
import { UpdateCustomerAssembler } from "./assembler";
|
||||
import { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
|
||||
import { CustomerPatchProps, CustomerService } from "../../../domain";
|
||||
import { CustomerFullPresenter } from "../../presenters";
|
||||
import { mapDTOToUpdateCustomerPatchProps } from "./map-dto-to-update-customer-props";
|
||||
|
||||
type UpdateCustomerUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
customer_id: string;
|
||||
dto: UpdateCustomerRequestDTO;
|
||||
dto: UpdateCustomerByIdRequestDTO;
|
||||
};
|
||||
|
||||
export class UpdateCustomerUseCase {
|
||||
constructor(
|
||||
private readonly service: CustomerService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly assembler: UpdateCustomerAssembler
|
||||
private readonly presenterRegistry: IPresenterRegistry
|
||||
) {}
|
||||
|
||||
public execute(params: UpdateCustomerUseCaseInput) {
|
||||
@ -28,6 +28,10 @@ export class UpdateCustomerUseCase {
|
||||
}
|
||||
|
||||
const customerId = idOrError.data;
|
||||
const presenter = this.presenterRegistry.getPresenter({
|
||||
resource: "customer",
|
||||
projection: "FULL",
|
||||
}) as CustomerFullPresenter;
|
||||
|
||||
// Mapear DTO → props de dominio
|
||||
const patchPropsResult = mapDTOToUpdateCustomerPatchProps(dto);
|
||||
@ -50,10 +54,10 @@ export class UpdateCustomerUseCase {
|
||||
return Result.fail(updatedCustomer.error);
|
||||
}
|
||||
|
||||
const savedCustomer = await this.service.saveCustomer(updatedCustomer.data, transaction);
|
||||
|
||||
const getDTO = this.assembler.toDTO(savedCustomer.data);
|
||||
return Result.ok(getDTO);
|
||||
const customerOrError = await this.service.saveCustomer(updatedCustomer.data, transaction);
|
||||
const customer = customerOrError.data;
|
||||
const dto = presenter.toOutput(customer);
|
||||
return Result.ok(dto);
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
CustomerFullPresenter,
|
||||
ListCustomersPresenter,
|
||||
ListCustomersUseCase,
|
||||
UpdateCustomerUseCase,
|
||||
} from "../application";
|
||||
import { GetCustomerUseCase } from "../application/use-cases/get-customer.use-case";
|
||||
import { CustomerService } from "../domain";
|
||||
@ -25,8 +26,8 @@ export type CustomerDeps = {
|
||||
list: () => ListCustomersUseCase;
|
||||
get: () => GetCustomerUseCase;
|
||||
create: () => CreateCustomerUseCase;
|
||||
/*update: () => UpdateCustomerUseCase;
|
||||
delete: () => DeleteCustomerUseCase;*/
|
||||
update: () => UpdateCustomerUseCase;
|
||||
//delete: () => DeleteCustomerUseCase;
|
||||
};
|
||||
};
|
||||
|
||||
@ -67,8 +68,8 @@ export function buildCustomerDependencies(params: ModuleParams): CustomerDeps {
|
||||
list: () => new ListCustomersUseCase(service, transactionManager, presenterRegistry),
|
||||
get: () => new GetCustomerUseCase(service, transactionManager, presenterRegistry),
|
||||
create: () => new CreateCustomerUseCase(service, transactionManager, presenterRegistry),
|
||||
/*update: () => new UpdateCustomerUseCase(_service!, transactionManager!, presenterRegistry!),
|
||||
delete: () => new DeleteCustomerUseCase(_service!, transactionManager!),*/
|
||||
update: () => new UpdateCustomerUseCase(service, transactionManager, presenterRegistry),
|
||||
//delete: () => new DeleteCustomerUseCase(_service!, transactionManager!),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -2,4 +2,4 @@ export * from "./create-customer.controller";
|
||||
export * from "./delete-customer.controller";
|
||||
export * from "./get-customer.controller";
|
||||
export * from "./list-customers.controller";
|
||||
///export * from "./update-customer.controller";
|
||||
export * from "./update-customer.controller";
|
||||
|
||||
@ -6,12 +6,15 @@ import {
|
||||
CreateCustomerRequestSchema,
|
||||
CustomerListRequestSchema,
|
||||
GetCustomerByIdRequestSchema,
|
||||
UpdateCustomerByIdParamsRequestSchema,
|
||||
UpdateCustomerByIdRequestSchema,
|
||||
} from "../../../common/dto";
|
||||
import { buildCustomerDependencies } from "../dependencies";
|
||||
import {
|
||||
CreateCustomerController,
|
||||
GetCustomerController,
|
||||
ListCustomersController,
|
||||
UpdateCustomerController,
|
||||
} from "./controllers";
|
||||
|
||||
export const customersRouter = (params: ModuleParams) => {
|
||||
@ -81,9 +84,10 @@ export const customersRouter = (params: ModuleParams) => {
|
||||
}
|
||||
);
|
||||
|
||||
/*router.put(
|
||||
router.put(
|
||||
"/:customer_id",
|
||||
//checkTabContext,
|
||||
|
||||
validateRequest(UpdateCustomerByIdParamsRequestSchema, "params"),
|
||||
validateRequest(UpdateCustomerByIdRequestSchema, "body"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
@ -93,7 +97,7 @@ export const customersRouter = (params: ModuleParams) => {
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
/*router.delete(
|
||||
"/:customer_id",
|
||||
//checkTabContext,
|
||||
|
||||
@ -103,7 +107,7 @@ export const customersRouter = (params: ModuleParams) => {
|
||||
const controller = new DeleteCustomerController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
); */
|
||||
);*/
|
||||
|
||||
app.use(`${baseRoutePath}/customers`, router);
|
||||
};
|
||||
|
||||
@ -31,4 +31,4 @@ export const UpdateCustomerByIdRequestSchema = z.object({
|
||||
currency_code: z.string().optional(),
|
||||
});
|
||||
|
||||
export type UpdateCustomerByIdRequestDTO = z.infer<typeof UpdateCustomerByIdRequestSchema>;
|
||||
export type UpdateCustomerByIdRequestDTO = Partial<z.infer<typeof UpdateCustomerByIdRequestSchema>>;
|
||||
|
||||
@ -21,6 +21,11 @@
|
||||
"title": "New customer",
|
||||
"description": "Create a new customer",
|
||||
"back_to_list": "Back to the list"
|
||||
},
|
||||
"update": {
|
||||
"title": "Update customer",
|
||||
"description": "Update a customer",
|
||||
"back_to_list": "Back to the list"
|
||||
}
|
||||
},
|
||||
"form_fields": {
|
||||
@ -117,10 +122,10 @@
|
||||
"placeholder": "Enter website URL",
|
||||
"description": "The website of the customer"
|
||||
},
|
||||
"default_tax": {
|
||||
"label": "Default tax",
|
||||
"placeholder": "Select default tax",
|
||||
"description": "The default tax rate for the customer"
|
||||
"default_taxes": {
|
||||
"label": "Default taxes",
|
||||
"placeholder": "Select default taxes",
|
||||
"description": "The default tax rates for the customer"
|
||||
},
|
||||
"language_code": {
|
||||
"label": "Language",
|
||||
@ -151,8 +156,8 @@
|
||||
"title": "Contact information",
|
||||
"description": "Customer contact details"
|
||||
},
|
||||
"additional_config": {
|
||||
"title": "Additional settings",
|
||||
"preferences": {
|
||||
"title": "Preferences",
|
||||
"description": "Additional customer configurations"
|
||||
}
|
||||
},
|
||||
|
||||
@ -21,6 +21,11 @@
|
||||
"title": "Nuevo cliente",
|
||||
"description": "Crear un nuevo cliente",
|
||||
"back_to_list": "Volver a la lista"
|
||||
},
|
||||
"update": {
|
||||
"title": "Modificación de cliente",
|
||||
"description": "Modificar los datos de un cliente",
|
||||
"back_to_list": "Back to the list"
|
||||
}
|
||||
},
|
||||
"form_fields": {
|
||||
@ -119,7 +124,7 @@
|
||||
"placeholder": "Ingrese la URL del sitio web",
|
||||
"description": "El sitio web del cliente"
|
||||
},
|
||||
"default_tax": {
|
||||
"default_taxes": {
|
||||
"label": "Impuesto por defecto",
|
||||
"placeholder": "Seleccione el impuesto por defecto",
|
||||
"description": "La tasa de impuesto por defecto para el cliente"
|
||||
@ -153,8 +158,8 @@
|
||||
"title": "Información de contacto",
|
||||
"description": "Detalles de contacto del cliente"
|
||||
},
|
||||
"additional_config": {
|
||||
"title": "Configuración adicional",
|
||||
"preferences": {
|
||||
"title": "Preferencias",
|
||||
"description": "Configuraciones adicionales del cliente"
|
||||
}
|
||||
},
|
||||
|
||||
@ -9,7 +9,7 @@ export const FormDebug = ({ form }: { form: UseFormReturn }) => {
|
||||
const currentValues = watch();
|
||||
|
||||
return (
|
||||
<div className='mt-6 p-4 border rounded bg-gray-50'>
|
||||
<div className='p-4 border rounded bg-red-50 mb-6'>
|
||||
<p>
|
||||
<strong>¿Formulario modificado?</strong> {isDirty ? "Sí" : "No"}
|
||||
</p>
|
||||
|
||||
@ -53,8 +53,8 @@ export const CustomerCreate = () => {
|
||||
<>
|
||||
<AppBreadcrumb />
|
||||
<AppContent>
|
||||
<div className='flex items-center justify-between space-y-2'>
|
||||
<div>
|
||||
<div className='flex items-center justify-between space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
|
||||
{t("pages.create.title")}
|
||||
</h2>
|
||||
|
||||
@ -7,17 +7,19 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { CURRENCY_OPTIONS, LANGUAGE_OPTIONS } from "../../constants";
|
||||
import { useTranslation } from "../../i18n";
|
||||
|
||||
export function CustomerAdditionalConfigFields({ control }: { control: any }) {
|
||||
export const CustomerAdditionalConfigFields = () => {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useForm();
|
||||
|
||||
return (
|
||||
<Card className='shadow-none'>
|
||||
<Card className='border-0 shadow-none bg-sidebar'>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("form_groups.additional_config.title")}</CardTitle>
|
||||
<CardDescription>{t("form_groups.additional_config.description")}</CardDescription>
|
||||
<CardTitle>{t("form_groups.preferences.title")}</CardTitle>
|
||||
<CardDescription>{t("form_groups.preferences.description")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
|
||||
<TaxesMultiSelectField
|
||||
@ -57,4 +59,4 @@ export function CustomerAdditionalConfigFields({ control }: { control: any }) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { TaxesMultiSelectField } from "@erp/core/components";
|
||||
import { TextField } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Card,
|
||||
@ -15,11 +16,183 @@ import {
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { useTranslation } from "../../i18n";
|
||||
|
||||
export function CustomerBasicInfoFields({ control }: { control: any }) {
|
||||
export const CustomerBasicInfoFields = ({ control }: { control: any }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card className='shadow-none'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Identificación</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='grid grid-cols-1 gap-6 md:grid-cols-4'>
|
||||
<div className='sm:col-span-full'>
|
||||
<FormField
|
||||
control={control}
|
||||
name='is_company'
|
||||
render={({ field }) => (
|
||||
<FormItem className='space-y-3'>
|
||||
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ? "1" : "0"}
|
||||
className='flex gap-6'
|
||||
>
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem value='1' />
|
||||
</FormControl>
|
||||
<FormLabel className='font-normal'>
|
||||
{t("form_fields.customer_type.company")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem value='0' />
|
||||
</FormControl>
|
||||
<FormLabel className='font-normal'>
|
||||
{t("form_fields.customer_type.individual")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='sm:col-span-2'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='name'
|
||||
required
|
||||
label={t("form_fields.name.label")}
|
||||
placeholder={t("form_fields.name.placeholder")}
|
||||
description={t("form_fields.name.description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='sm:col-span-2'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='trade_name'
|
||||
label={t("form_fields.trade_name.label")}
|
||||
placeholder={t("form_fields.trade_name.placeholder")}
|
||||
description={t("form_fields.trade_name.description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='sm:col-span-2'>
|
||||
<TaxesMultiSelectField
|
||||
control={control}
|
||||
name='default_taxes'
|
||||
required
|
||||
label={t("form_fields.default_taxes.label")}
|
||||
placeholder={t("form_fields.default_taxes.placeholder")}
|
||||
description={t("form_fields.default_taxes.description")}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-auto'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='reference'
|
||||
label={t("form_fields.reference.label")}
|
||||
placeholder={t("form_fields.reference.placeholder")}
|
||||
description={t("form_fields.reference.description")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='space-y-12'>
|
||||
<div className='border-b border-gray-900/10 pb-12 dark:border-white/10'>
|
||||
<h2 className='text-base/7 font-semibold text-gray-900 dark:text-white'>
|
||||
{t("form_groups.basic_info.title")}
|
||||
</h2>
|
||||
<p className='mt-1 text-sm/6 text-gray-600 dark:text-gray-400'>
|
||||
{t("form_groups.basic_info.description")}
|
||||
</p>
|
||||
|
||||
<div className='mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6'>
|
||||
<div className='sm:col-span-6'>
|
||||
<FormField
|
||||
control={control}
|
||||
name='is_company'
|
||||
render={({ field }) => (
|
||||
<FormItem className='space-y-3'>
|
||||
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ? "1" : "0"}
|
||||
className='flex gap-6'
|
||||
>
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem value='1' />
|
||||
</FormControl>
|
||||
<FormLabel className='font-normal'>
|
||||
{t("form_fields.customer_type.company")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormControl>
|
||||
<RadioGroupItem value='0' />
|
||||
</FormControl>
|
||||
<FormLabel className='font-normal'>
|
||||
{t("form_fields.customer_type.individual")}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='sm:col-span-2'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='name'
|
||||
required
|
||||
label={t("form_fields.name.label")}
|
||||
placeholder={t("form_fields.name.placeholder")}
|
||||
description={t("form_fields.name.description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='sm:col-span-2'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='trade_name'
|
||||
label={t("form_fields.trade_name.label")}
|
||||
placeholder={t("form_fields.trade_name.placeholder")}
|
||||
description={t("form_fields.trade_name.description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='col-span-full'>
|
||||
<TextField
|
||||
control={control}
|
||||
name='reference'
|
||||
label={t("form_fields.reference.label")}
|
||||
placeholder={t("form_fields.reference.placeholder")}
|
||||
description={t("form_fields.reference.description")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className='shadow-sm bg-gray-50/50'>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("form_groups.basic_info.title")}</CardTitle>
|
||||
<CardDescription>{t("form_groups.basic_info.description")}</CardDescription>
|
||||
@ -85,4 +258,4 @@ export function CustomerBasicInfoFields({ control }: { control: any }) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,15 +1,28 @@
|
||||
import { TextField } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
|
||||
import { TextField } from "@repo/rdx-ui/components";
|
||||
import { Input } from "@repo/shadcn-ui/components";
|
||||
import { ChevronDown, Phone } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "../../i18n";
|
||||
|
||||
export function CustomerContactFields({ control }: { control: any }) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<Card className='shadow-none'>
|
||||
@ -18,6 +31,63 @@ export function CustomerContactFields({ control }: { control: any }) {
|
||||
<CardDescription>{t("form_groups.contact_info.description")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
|
||||
<Collapsible open={open} onOpenChange={setOpen} className='space-y-4'>
|
||||
<CollapsibleTrigger className='inline-flex items-center gap-1 text-sm text-muted-foreground hover:underline'>
|
||||
Más detalles{" "}
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`} />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className='grid grid-cols-1 gap-6 md:grid-cols-2'>
|
||||
<FormField
|
||||
control={control}
|
||||
name='phone2'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Teléfono secundario</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
icon={<Phone className='h-4 w-4 text-muted-foreground' />}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name='mobile2'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Móvil secundario</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder='+34 600 00 000'
|
||||
icon={<Phone className='h-4 w-4 text-muted-foreground' />}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name='fax'
|
||||
render={({ field }) => (
|
||||
<FormItem className='md:col-span-2'>
|
||||
<FormLabel>Fax</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<TextField
|
||||
control={control}
|
||||
name='email_primary'
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { FieldErrors, useForm } from "react-hook-form";
|
||||
|
||||
@ -13,21 +22,13 @@ import { CustomerBasicInfoFields } from "./customer-basic-info-fields";
|
||||
import { CustomerContactFields } from "./customer-contact-fields";
|
||||
|
||||
interface CustomerFormProps {
|
||||
formId: string;
|
||||
defaultValues: CustomerData; // ✅ ya no recibe DTO
|
||||
isPending?: boolean;
|
||||
onSubmit: (data: CustomerData) => void;
|
||||
onSubmit: (data: CustomerUpdateData) => void;
|
||||
onError: (errors: FieldErrors<CustomerUpdateData>) => void;
|
||||
errorMessage?: string; // ✅ prop nueva para mostrar error global
|
||||
}
|
||||
|
||||
export const CustomerEditForm = ({
|
||||
formId,
|
||||
defaultValues,
|
||||
onSubmit,
|
||||
isPending,
|
||||
errorMessage,
|
||||
}: CustomerFormProps) => {
|
||||
export const CustomerEditForm = ({ defaultValues, onSubmit, isPending }: CustomerFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<CustomerUpdateData>({
|
||||
@ -36,14 +37,85 @@ export const CustomerEditForm = ({
|
||||
disabled: isPending,
|
||||
});
|
||||
|
||||
const {
|
||||
watch,
|
||||
formState: { isDirty, dirtyFields },
|
||||
} = form;
|
||||
|
||||
useUnsavedChangesNotifier({
|
||||
isDirty: form.formState.isDirty,
|
||||
isDirty,
|
||||
});
|
||||
|
||||
const currentValues = watch();
|
||||
|
||||
const handleSubmit = (data: CustomerUpdateData) => {
|
||||
console.log("Datos del formulario:", data);
|
||||
const changedData: Record<string, string> = {};
|
||||
|
||||
Object.keys(dirtyFields).forEach((field) => {
|
||||
const value = String(currentValues[field as keyof CustomerUpdateData]);
|
||||
changedData[field] = value;
|
||||
});
|
||||
|
||||
console.log(changedData);
|
||||
|
||||
onSubmit(changedData);
|
||||
};
|
||||
|
||||
const handleError = (errors: FieldErrors<CustomerUpdateData>) => {
|
||||
console.error("Errores en el formulario:", errors);
|
||||
// Aquí puedes manejar los errores, por ejemplo, mostrar un mensaje al usuario
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.reset(defaultValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormDebug form={form} />
|
||||
<form id={formId} onSubmit={form.handleSubmit(onSubmit, oError)}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
||||
<div className='flex gap-6'>
|
||||
<div className='w-full xl:w-2/3 space-y-12'>
|
||||
<CustomerBasicInfoFields control={form.control} />
|
||||
<CustomerContactFields control={form.control} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormDebug form={form} />
|
||||
<form id={formId} onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
||||
<div className='grid grid-cols-3 gap-12'>
|
||||
<div className='col-span-2'>
|
||||
<Card className='border-0 shadow-none bg-background'>
|
||||
<CardHeader className='px-0'>
|
||||
<CardTitle>{t("form_groups.basic_info.title")}</CardTitle>
|
||||
<CardDescription>{t("form_groups.basic_info.description")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='px-0'>
|
||||
<CustomerBasicInfoFields />
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<p> </p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
<div>
|
||||
<CustomerAdditionalConfigFields />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormDebug form={form} />
|
||||
<form id={formId} onSubmit={form.handleSubmit(handleSubmit, handleError)}>
|
||||
<div className='w-full grid grid-cols-1 space-y-8 space-x-8 xl:grid-cols-2'>
|
||||
<CustomerBasicInfoFields control={form.control} />
|
||||
<CustomerAddressFields control={form.control} />
|
||||
|
||||
@ -94,8 +94,8 @@ export const CustomerUpdate = () => {
|
||||
<>
|
||||
<AppBreadcrumb />
|
||||
<AppContent>
|
||||
<div className='flex items-center justify-between space-y-2'>
|
||||
<div>
|
||||
<div className='flex items-center justify-between space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<h2 className='text-2xl font-bold tracking-tight text-balance scroll-m-2'>
|
||||
{t("pages.update.title")}
|
||||
</h2>
|
||||
@ -132,11 +132,10 @@ export const CustomerUpdate = () => {
|
||||
<div className='flex flex-1 flex-col gap-4 p-4'>
|
||||
{/* Importante: proveemos un formId para que el botón del header pueda hacer submit */}
|
||||
<CustomerEditForm
|
||||
formId='customer-edit-form'
|
||||
defaultValues={defaultValues}
|
||||
defaultValues={customerData}
|
||||
onSubmit={handleSubmit}
|
||||
onError={handleError}
|
||||
isPending={isUpdating}
|
||||
errorMessage={isUpdateError ? getErrorMessage(updateError) : undefined}
|
||||
/>
|
||||
</div>
|
||||
</AppContent>
|
||||
|
||||
@ -71,6 +71,17 @@ export class UtcDate extends ValueObject<UtcDateProps> {
|
||||
return this.date.toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve la fecha en formato dd/mm/yyyy (formato europeo).
|
||||
*/
|
||||
toEuropeanString(): string {
|
||||
const day = String(this.date.getUTCDate()).padStart(2, "0");
|
||||
const month = String(this.date.getUTCMonth() + 1).padStart(2, "0"); // Los meses en JS empiezan en 0
|
||||
const year = this.date.getUTCFullYear();
|
||||
|
||||
return `${day}/${month}/${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compara si dos instancias de UtcDate son iguales.
|
||||
*/
|
||||
|
||||
@ -53,7 +53,12 @@ export function TextField<TFormValues extends FieldValues>({
|
||||
</div>
|
||||
)}
|
||||
<FormControl>
|
||||
<Input disabled={isDisabled} placeholder={placeholder} {...field} />
|
||||
<Input
|
||||
disabled={isDisabled}
|
||||
placeholder={placeholder}
|
||||
{...field}
|
||||
className='placeholder:font-normal placeholder:italic'
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<p className={cn("text-xs text-muted-foreground", !description && "invisible")}>
|
||||
|
||||
@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
type={type}
|
||||
data-slot='input'
|
||||
className={cn(
|
||||
"bg-background text-foreground",
|
||||
"bg-input text-foreground",
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
|
||||
@ -50,9 +50,9 @@
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
/*--font-sans: Geist, sans-serif;
|
||||
--font-serif: Merriweather, serif;
|
||||
--font-mono: "Geist Mono", monospace;*/
|
||||
--font-sans: Roboto, sans-serif;
|
||||
--font-serif: Domine, serif;
|
||||
--font-mono: "Roboto Mono", monospace;
|
||||
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
@ -92,10 +92,10 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.5rem;
|
||||
--radius: 0.3rem;
|
||||
--background: oklch(1.0 0.0 0);
|
||||
--foreground: oklch(0.143 0.003 271.9282674829111);
|
||||
--card: oklch(1.0 0.0 0);
|
||||
--card: oklch(0.977 0.007 272.5840410480741);
|
||||
--card-foreground: oklch(0.143 0.003 271.9282674829111);
|
||||
--popover: oklch(1.0 0.0 0);
|
||||
--popover-foreground: oklch(0.143 0.003 271.9282674829111);
|
||||
@ -165,16 +165,13 @@
|
||||
@apply border-border outline-ring/50;
|
||||
@apply transition-colors duration-300; /* Added transition for smooth color changes */
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
label {
|
||||
@apply font-light;
|
||||
@apply bg-input;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user