prueba tecnica
This commit is contained in:
1
front/contracts-frontend/.env
Normal file
1
front/contracts-frontend/.env
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL=http://localhost:5009/api
|
||||
1
front/contracts-frontend/.env.production
Normal file
1
front/contracts-frontend/.env.production
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL=https://quediaempiezo.asarmientotest.es/api
|
||||
24
front/contracts-frontend/.gitignore
vendored
Normal file
24
front/contracts-frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
54
front/contracts-frontend/README.md
Normal file
54
front/contracts-frontend/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
extends: [
|
||||
// Remove ...tseslint.configs.recommended and replace with this
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default tseslint.config({
|
||||
plugins: {
|
||||
// Add the react-x and react-dom plugins
|
||||
'react-x': reactX,
|
||||
'react-dom': reactDom,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended typescript rules
|
||||
...reactX.configs['recommended-typescript'].rules,
|
||||
...reactDom.configs.recommended.rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
28
front/contracts-frontend/eslint.config.js
Normal file
28
front/contracts-frontend/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
13
front/contracts-frontend/index.html
Normal file
13
front/contracts-frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/proximaenergia.com.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Prueba técnica proxima contracts</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3786
front/contracts-frontend/package-lock.json
generated
Normal file
3786
front/contracts-frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
front/contracts-frontend/package.json
Normal file
33
front/contracts-frontend/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "contracts-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-router-dom": "^7.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/node": "^24.0.1",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
BIN
front/contracts-frontend/public/proximaenergia.com.ico
Normal file
BIN
front/contracts-frontend/public/proximaenergia.com.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 889 B |
59
front/contracts-frontend/src/components/forms/FormField.tsx
Normal file
59
front/contracts-frontend/src/components/forms/FormField.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import type {
|
||||
FieldError,
|
||||
FieldValues,
|
||||
Path,
|
||||
RegisterOptions,
|
||||
UseFormRegister,
|
||||
} from 'react-hook-form';
|
||||
|
||||
type FormFieldProps<T extends FieldValues> = {
|
||||
label: string;
|
||||
name: Path<T>;
|
||||
register: UseFormRegister<T>;
|
||||
error?: FieldError;
|
||||
rules?: RegisterOptions<T>;
|
||||
type?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function FormField<T extends FieldValues>({
|
||||
label,
|
||||
name,
|
||||
register,
|
||||
error,
|
||||
rules,
|
||||
type = 'text',
|
||||
className = '',
|
||||
}: FormFieldProps<T>) {
|
||||
const buildValidationRules = (): RegisterOptions<T, Path<T>> => {
|
||||
const base: RegisterOptions<T, Path<T>> = { required: 'Obligatorio' };
|
||||
|
||||
if (type === 'number') {
|
||||
(base as any).valueAsNumber = true;
|
||||
}
|
||||
if (rules) Object.assign(base, rules);
|
||||
|
||||
return base;
|
||||
};
|
||||
|
||||
const validationRules = buildValidationRules();
|
||||
|
||||
return (
|
||||
<div className={`col-12 col-md-6 col-lg-4 ${className}`}>
|
||||
<label htmlFor={name} className="form-label">
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<input
|
||||
id={name}
|
||||
type={type}
|
||||
className={`form-control ${error ? 'is-invalid' : ''}`}
|
||||
{...register(name, validationRules)}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<small className="text-danger">{error.message ?? 'Obligatorio'}</small>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
front/contracts-frontend/src/env.d.ts
vendored
Normal file
7
front/contracts-frontend/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
import { get, post, put } from '../../../utils/http/Http';
|
||||
|
||||
import type { ContractSummary } from '../types/ContractSummary';
|
||||
import type { ContractDetail } from '../types/ContractDetail';
|
||||
import type { UpdateContractDto } from '../types/UpdateContractDto';
|
||||
import type { CreateContractDto } from '../types/CreateContractDto';
|
||||
|
||||
|
||||
export const getAllContracts = () =>
|
||||
get<ContractSummary[]>('/Contracts/GetAll');
|
||||
|
||||
|
||||
export const getContractById = (id: number) =>
|
||||
get<ContractDetail>('/Contracts/GetById', { params: { id } });
|
||||
|
||||
|
||||
export const updateContract = (dto: UpdateContractDto) =>
|
||||
put<void, UpdateContractDto>('/Contracts/UpdateContract', dto);
|
||||
|
||||
|
||||
export const createContract = (dto: CreateContractDto) =>
|
||||
post<{ newContractId: number }, CreateContractDto>(
|
||||
'/Contracts/CreateContract',
|
||||
dto
|
||||
);
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import type { CreateContractDto } from '@/features/contracts/types/CreateContractDto';
|
||||
import FormField from '@/components/forms/FormField';
|
||||
import { useRates } from '@/features/rates/hooks/useRates';
|
||||
import { useCreateContract } from '@/features/contracts/hooks/useCreateContracts';
|
||||
import { validateNifNie } from '@/utils/validations/ValidateNifNie';
|
||||
import { maxLengthNonEmpty } from '@/utils/validations/MaxLengthNonEmpty';
|
||||
|
||||
|
||||
export default function ContractCreateForm() {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<CreateContractDto>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { rates, loading: loadingRates, error: rateError } = useRates();
|
||||
|
||||
const { create, loading: creating, error: submitError } = useCreateContract();
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(create)} className="row g-3">
|
||||
|
||||
<FormField<CreateContractDto>
|
||||
label="Nombre"
|
||||
name="contractorName"
|
||||
register={register}
|
||||
error={errors.contractorName}
|
||||
rules={{validate: maxLengthNonEmpty(50)}}
|
||||
/>
|
||||
|
||||
<FormField<CreateContractDto>
|
||||
label="Apellidos"
|
||||
name="contractorSurname"
|
||||
register={register}
|
||||
error={errors.contractorSurname}
|
||||
rules={{validate: maxLengthNonEmpty(100)}}
|
||||
/>
|
||||
|
||||
<FormField<CreateContractDto>
|
||||
label="DNI / NIE"
|
||||
name="contractorIdNumber"
|
||||
register={register}
|
||||
error={errors.contractorIdNumber}
|
||||
rules={{
|
||||
validate: {
|
||||
nifNie : validateNifNie,
|
||||
maxLengthNonEmpty: maxLengthNonEmpty(9)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="col-12 col-md-6 col-lg-4">
|
||||
<label htmlFor="rateId" className="form-label">
|
||||
Tarifa
|
||||
</label>
|
||||
<select
|
||||
id="rateId"
|
||||
className="form-select"
|
||||
{...register('rateId', { required: true, valueAsNumber: true })}
|
||||
disabled={loadingRates || !!rateError}
|
||||
>
|
||||
<option value="">
|
||||
{loadingRates ? 'Cargando…' : '— Selecciona —'}
|
||||
</option>
|
||||
{rates.map((rate) => (
|
||||
<option key={rate.id} value={rate.id}>
|
||||
{rate.name} ({rate.price.toFixed(2)} €/kWh)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.rateId && <small className="text-danger">Obligatorio</small>}
|
||||
{rateError && <small className="text-danger">{rateError}</small>}
|
||||
</div>
|
||||
|
||||
{submitError && (
|
||||
<div className="col-12">
|
||||
<small className="text-danger">{submitError}</small>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="col-12 mt-2">
|
||||
<button type="submit" className="btn btn-primary me-2" disabled={creating}>
|
||||
{creating ? 'Guardando…' : 'Guardar'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={() => navigate(-1)}
|
||||
disabled={creating}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ContractDetail } from '@/features/contracts/types/ContractDetail';
|
||||
|
||||
|
||||
export default function ContractDetailComponent({
|
||||
contract,
|
||||
onEdit,
|
||||
}: {
|
||||
contract: ContractDetail;
|
||||
onEdit: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<h4 className="card-title mb-3">Contrato #{contract.id}</h4>
|
||||
<p><strong>Nif/Nie:</strong> {contract.contractorIdNumber}</p>
|
||||
<p><strong>Nombre:</strong> {contract.contractorName}</p>
|
||||
<p><strong>Apellidos:</strong> {contract.contractorSurname}</p>
|
||||
<p><strong>Inicio:</strong> {new Date(contract.contractInitDate).toLocaleDateString()}</p>
|
||||
<p><strong>Tarifa:</strong> {contract.rateId}, {contract.rateName} </p>
|
||||
|
||||
|
||||
<button onClick={onEdit} className="btn btn-primary mt-3">
|
||||
Editar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { ContractDetail } from '@/features/contracts/types/ContractDetail';
|
||||
import type { UpdateContractDto } from '@/features/contracts/types/UpdateContractDto';
|
||||
import { updateContract } from '@/features/contracts/api/ContractsApi';
|
||||
import { getProblemMessage } from '@/utils/http/ErrorHelper';
|
||||
|
||||
import FormField from '@/components/forms/FormField';
|
||||
import { maxLengthNonEmpty } from '@/utils/validations/MaxLengthNonEmpty';
|
||||
import { maxDate, minDate } from '@/utils/validations/dateValidators';
|
||||
|
||||
interface Props {
|
||||
contract: ContractDetail;
|
||||
onSaved: (updated: ContractDetail) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
contractorName: string;
|
||||
contractorSurname: string;
|
||||
contractInitDate: string;
|
||||
rateId: number;
|
||||
}
|
||||
|
||||
export default function ContractUpdateComponent({
|
||||
contract,
|
||||
onSaved,
|
||||
onCancel,
|
||||
}: Props) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
reset,
|
||||
} = useForm<FormData>();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
contractorName: contract.contractorName,
|
||||
contractorSurname: contract.contractorSurname,
|
||||
contractInitDate: contract.contractInitDate.slice(0, 10),
|
||||
rateId: contract.rateId,
|
||||
});
|
||||
}, [contract, reset]);
|
||||
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
const dto: UpdateContractDto = { contractId: contract.id, ...data };
|
||||
try {
|
||||
await updateContract(dto);
|
||||
onSaved({ ...contract, ...data });
|
||||
} catch (err) {
|
||||
alert(getProblemMessage(err, 'Error al actualizar.'));
|
||||
}
|
||||
};
|
||||
|
||||
const todayIso = new Date().toISOString().slice(0, 10);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="card shadow-sm p-4">
|
||||
<h4 className="mb-4">Editar contrato #{contract.id}</h4>
|
||||
|
||||
<div className="row gy-3">
|
||||
<FormField<FormData>
|
||||
label="Nombre"
|
||||
name="contractorName"
|
||||
register={register}
|
||||
error={errors.contractorName}
|
||||
rules={{
|
||||
validate: maxLengthNonEmpty(50),
|
||||
}}
|
||||
className="col-12"
|
||||
/>
|
||||
|
||||
<FormField<FormData>
|
||||
label="Apellidos"
|
||||
name="contractorSurname"
|
||||
register={register}
|
||||
error={errors.contractorSurname}
|
||||
rules={{
|
||||
validate: maxLengthNonEmpty(100),
|
||||
}}
|
||||
className="col-12"
|
||||
/>
|
||||
|
||||
<FormField<FormData>
|
||||
label="Inicio"
|
||||
name="contractInitDate"
|
||||
type="date"
|
||||
register={register}
|
||||
error={errors.contractInitDate}
|
||||
rules={{
|
||||
required: 'Obligatorio',
|
||||
validate: {
|
||||
minDate: minDate("01/01/1999"),
|
||||
maxDate: maxDate(todayIso)
|
||||
}
|
||||
}}
|
||||
className="col-12 col-md-6"
|
||||
/>
|
||||
|
||||
<FormField<FormData>
|
||||
label="Rate ID"
|
||||
name="rateId"
|
||||
type="number"
|
||||
register={register}
|
||||
error={errors.rateId}
|
||||
rules={{
|
||||
required: 'Obligatorio',
|
||||
valueAsNumber: true,
|
||||
min: { value: 1, message: 'Debe ser ≥ 1' },
|
||||
}}
|
||||
className="col-12 col-md-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="d-flex gap-2 mt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="btn btn-success"
|
||||
>
|
||||
{isSubmitting ? 'Guardando…' : 'Guardar'}
|
||||
</button>
|
||||
|
||||
<button type="button" onClick={onCancel} className="btn btn-secondary">
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useContractsAll } from '../hooks/useContractsAll';
|
||||
|
||||
export default function ContractsTableComponent() {
|
||||
const { contracts, loading, error } = useContractsAll();
|
||||
|
||||
if (loading) return <p>Cargando…</p>;
|
||||
if (error) return <p className="text-danger">{error}</p>;
|
||||
|
||||
return (
|
||||
<table className="table table-hover table-contracts">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nombre</th>
|
||||
<th>Apellidos</th>
|
||||
<th>Inicio</th>
|
||||
<th>Tarifa</th>
|
||||
<th className="text-center">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{contracts.map(c => (
|
||||
<tr key={c.id}>
|
||||
<td>{c.id}</td>
|
||||
<td>{c.contractorName}</td>
|
||||
<td>{c.contractorSurname}</td>
|
||||
<td>{new Date(c.contractInitDate).toLocaleDateString()}</td>
|
||||
<td>{c.rateName}</td>
|
||||
<td className="text-center">
|
||||
<Link to={`/contracts/${c.id}`} className="btn btn-sm btn-primary">
|
||||
Detalle
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { getContractById } from '../api/ContractsApi';
|
||||
import type { ContractDetail } from '../types/ContractDetail';
|
||||
|
||||
export function useContractDetail(contractId: number) {
|
||||
const [contract, setContract] = useState<ContractDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchContract = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getContractById(contractId);
|
||||
setContract(data);
|
||||
setError(null);
|
||||
} catch {
|
||||
setError('Contrato no encontrado');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [contractId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchContract();
|
||||
}, [fetchContract]);
|
||||
|
||||
return { contract, loading, error, refresh: fetchContract };
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getAllContracts } from '@/features/contracts/api/ContractsApi';
|
||||
import type { ContractSummary } from '@/features/contracts/types/ContractSummary';
|
||||
import { getProblemMessage } from '@/utils/http/ErrorHelper';
|
||||
import { HttpError } from '@/utils/http/HttpError';
|
||||
|
||||
export function useContractsAll() {
|
||||
const [contracts, setContractsAll] = useState<ContractSummary[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setContractsAll(await getAllContracts());
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof HttpError && err.status === 404
|
||||
? 'No se encontraron contratos.'
|
||||
: getProblemMessage(err, 'Error al cargar los contratos.')
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return { contracts, loading, error };
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { createContract as apiCreate } from '../api/ContractsApi';
|
||||
import type { CreateContractDto } from '../types/CreateContractDto';
|
||||
import { getProblemMessage } from '@/utils/http/ErrorHelper';
|
||||
|
||||
export function useCreateContract() {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const create = async (dto: CreateContractDto) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { newContractId } = await apiCreate(dto);
|
||||
navigate(`/contracts/${newContractId}`);
|
||||
} catch (err) {
|
||||
setError(getProblemMessage(err, 'No se pudo crear el contrato'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { create, loading, error };
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import ContractCreateForm from '@/features/contracts/components/ContractCreateForm';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
|
||||
export default function ContractCreate() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="container-fluid py-4 px-4 px-lg-5">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
|
||||
{/* Cabecera */}
|
||||
<div className="d-flex align-items-center mb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(-1)}
|
||||
className="btn btn-outline-primary btn-sm me-3"
|
||||
>
|
||||
← Atrás
|
||||
</button>
|
||||
<h2 className="h4 m-0">Crear nuevo contrato</h2>
|
||||
</div>
|
||||
|
||||
{/* Formulario */}
|
||||
<ContractCreateForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// src/features/contracts/components/ContractDetail.tsx
|
||||
import { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useContractDetail } from '@/features/contracts/hooks/useContractDetail';
|
||||
import ContractDetailComponent from '@/features/contracts/components/ContractDetailComponent';
|
||||
import ContractUpdateComponent from '@/features/contracts/components/ContractUpdateComponent';
|
||||
|
||||
export default function ContractDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const numericId = Number(id);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { contract, loading, error, refresh } = useContractDetail(numericId);
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
if (loading) return <p>Cargando…</p>;
|
||||
if (error || !contract) return <p className="text-red-600">{error}</p>;
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-xl space-y-6 p-4">
|
||||
{editing ? (
|
||||
<div className="container my-4">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<div className="d-flex align-items-center my-4">
|
||||
<h2 className="h4 m-0">Modificar contrato</h2>
|
||||
</div>
|
||||
<ContractUpdateComponent
|
||||
contract={contract}
|
||||
onSaved={async () => {
|
||||
setEditing(false);
|
||||
await refresh();
|
||||
}}
|
||||
onCancel={() => setEditing(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="container my-4">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<div className="d-flex align-items-center my-4">
|
||||
<button
|
||||
onClick={() => navigate(`/`)}
|
||||
className="btn btn-outline-primary btn-sm me-3"
|
||||
>
|
||||
← Atrás
|
||||
</button>
|
||||
<h2 className="h4 m-0">Detalle de contrato</h2>
|
||||
</div>
|
||||
<ContractDetailComponent
|
||||
contract={contract}
|
||||
onEdit={() => setEditing(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import ContractsTableComponent from '@/features/contracts/components/ContractsTableComponent';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
||||
export default function ContractsList() {
|
||||
return (
|
||||
<div className="container my-4">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<h2 className="h4 my-4">Listado de contratos</h2>
|
||||
<Link to="/contracts/new" className="btn btn-primary mb-3">
|
||||
+ Nuevo contrato
|
||||
</Link>
|
||||
<ContractsTableComponent />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ContractSummary } from "./ContractSummary";
|
||||
|
||||
export interface ContractDetail extends ContractSummary {
|
||||
contractorIdNumber: string;
|
||||
rateId: number;
|
||||
ratePrice: number;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface ContractSummary {
|
||||
id: number;
|
||||
contractorName: string;
|
||||
contractorSurname: string;
|
||||
contractInitDate: string;
|
||||
rateName: string;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface CreateContractDto {
|
||||
contractorIdNumber: string;
|
||||
contractorName: string;
|
||||
contractorSurname: string;
|
||||
rateId: number;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface UpdateContractDto {
|
||||
contractId: number;
|
||||
rateId: number;
|
||||
contractorIdNumber?: string;
|
||||
contractorName?: string;
|
||||
contractorSurname?: string;
|
||||
contractInitDate?: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import {get} from '@/utils/http/Http'
|
||||
import type { RateDetails } from '../types/RateDetails'
|
||||
|
||||
export const getAllRates = () => get<RateDetails[]>('/Rates/GetAllRates');
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getAllRates } from '../api/RatesApi';
|
||||
import type { RateDetails } from '../types/RateDetails';
|
||||
import { getProblemMessage } from '@/utils/http/ErrorHelper';
|
||||
|
||||
export function useRates() {
|
||||
const [rates, setRates] = useState<RateDetails[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setRates(await getAllRates());
|
||||
} catch (err) {
|
||||
setError(getProblemMessage(err, 'No se pudieron cargar las tarifas'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return { rates, loading, error };
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface RateDetails {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
}
|
||||
14
front/contracts-frontend/src/main.tsx
Normal file
14
front/contracts-frontend/src/main.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import '@/styles/brand-proxima.css';
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import AppRoutes from './routes/Index.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
</StrictMode>,
|
||||
)
|
||||
14
front/contracts-frontend/src/routes/Index.tsx
Normal file
14
front/contracts-frontend/src/routes/Index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import ContractsList from '@/features/contracts/pages/ContractsList';
|
||||
import ContractDetail from '@/features/contracts/pages/ContractDetail';
|
||||
import ContractCreate from '@/features/contracts/pages/ContractCreate';
|
||||
|
||||
export default function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<ContractsList />} />
|
||||
<Route path="/contracts/:id" element={<ContractDetail />} />
|
||||
<Route path="/contracts/new" element={<ContractCreate />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
58
front/contracts-frontend/src/styles/brand-proxima.css
Normal file
58
front/contracts-frontend/src/styles/brand-proxima.css
Normal file
@@ -0,0 +1,58 @@
|
||||
:root {
|
||||
--brand-bg: #051622;
|
||||
--brand-bg-alt: #0f2432;
|
||||
--brand-bg-hover: #2d6588;
|
||||
--brand-border: #113345;
|
||||
|
||||
--brand-text: #e5f2f7;
|
||||
--brand-heading: #ffffff;
|
||||
|
||||
|
||||
--brand-primary: #00d68f;
|
||||
--brand-primary-hover: #00b67a;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bs-body-bg: var(--brand-bg);
|
||||
--bs-body-color: var(--brand-text);
|
||||
--bs-border-color: var(--brand-border);
|
||||
--bs-heading-color: var(--brand-heading);
|
||||
|
||||
--bs-primary: var(--brand-primary);
|
||||
--bs-primary-rgb: 0,214,143;
|
||||
--bs-link-color: var(--brand-primary);
|
||||
--bs-link-hover-color: var(--brand-primary-hover);
|
||||
}
|
||||
|
||||
|
||||
.table-contracts {
|
||||
--bs-table-color: var(--brand-text);
|
||||
--bs-table-bg: transparent;
|
||||
--bs-table-border-color: var(--brand-border);
|
||||
}
|
||||
|
||||
.table-contracts > thead > tr {
|
||||
background-color: #0b1e2c;
|
||||
color: var(--brand-heading);
|
||||
}
|
||||
|
||||
.table-contracts > tbody > tr:nth-of-type(odd) { background-color: var(--brand-bg-alt); }
|
||||
.table-contracts > tbody > tr:nth-of-type(even) { background-color: var(--brand-bg); }
|
||||
|
||||
.table-contracts > tbody > tr:hover {
|
||||
background-color: var(--brand-bg-hover);
|
||||
outline: 1px solid var(--brand-primary-hover);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--brand-primary);
|
||||
border-color: var(--brand-primary);
|
||||
color:#051622
|
||||
}
|
||||
.btn-primary:hover,
|
||||
.btn-primary:focus {
|
||||
background-color: var(--brand-primary-hover);
|
||||
border-color: var(--brand-primary-hover);
|
||||
}
|
||||
17
front/contracts-frontend/src/utils/http/ErrorHelper.ts
Normal file
17
front/contracts-frontend/src/utils/http/ErrorHelper.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { HttpError } from '@/utils/http/HttpError';
|
||||
|
||||
export function getProblemMessage(err: unknown, fallback = 'Error inesperado') {
|
||||
console.log("Hemos entrado aqui")
|
||||
if (err instanceof HttpError) {
|
||||
console.log(
|
||||
err
|
||||
)
|
||||
console.log(
|
||||
err.body
|
||||
)
|
||||
return err.body?.detail
|
||||
?? err.body?.title
|
||||
?? err.message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
87
front/contracts-frontend/src/utils/http/Http.ts
Normal file
87
front/contracts-frontend/src/utils/http/Http.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { HttpError } from "./HttpError";
|
||||
import type { ProblemDetails } from './ProblemDetails'
|
||||
|
||||
|
||||
|
||||
type Params = Record<string, string | number | boolean | undefined>;
|
||||
|
||||
function buildQuery(params?: Params): string {
|
||||
if (!params) return '';
|
||||
|
||||
const usp = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined) usp.append(key, String(value));
|
||||
}
|
||||
const qs = usp.toString();
|
||||
return qs ? `?${qs}` : '';
|
||||
}
|
||||
|
||||
interface RequestOptions<T> {
|
||||
params?: Params;
|
||||
body?: T;
|
||||
headers?: HeadersInit;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? '/api';
|
||||
|
||||
|
||||
|
||||
async function request<R, B = unknown>(
|
||||
method: 'GET' | 'POST' | 'PUT',
|
||||
url: string,
|
||||
{ params, body, headers, timeout = 15_000 }: RequestOptions<B> = {}
|
||||
): Promise<R> {
|
||||
const controller = new AbortController();
|
||||
const id =
|
||||
timeout >= 0 ? setTimeout(() => controller.abort(), timeout) : null;
|
||||
|
||||
try {
|
||||
const q = buildQuery(params);
|
||||
|
||||
const res = await fetch(`${BASE_URL}${url}${q}`, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
signal: controller.signal,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
const contentType = res.headers.get('Content-Type') ?? '';
|
||||
const isJson = /application\/(json|problem\+json)/i.test(contentType);
|
||||
|
||||
const raw = await res.text();
|
||||
|
||||
if (!res.ok) {
|
||||
const problem: ProblemDetails | null =
|
||||
isJson && raw ? JSON.parse(raw) : null;
|
||||
throw new HttpError(res.status, res.statusText, problem);
|
||||
}
|
||||
|
||||
const data: R =
|
||||
isJson && raw ? (JSON.parse(raw) as R) : (undefined as unknown as R);
|
||||
|
||||
return data;
|
||||
} finally {
|
||||
if (id) clearTimeout(id);
|
||||
}
|
||||
}
|
||||
|
||||
export const get = <R>(url: string, options?: RequestOptions<never>) =>
|
||||
request<R>('GET', url, options);
|
||||
|
||||
export const post = <R, B = unknown>(
|
||||
url: string,
|
||||
body: B,
|
||||
options?: Omit<RequestOptions<B>, 'body'>
|
||||
) => request<R, B>('POST', url, { ...options, body });
|
||||
|
||||
export const put = <R, B = unknown>(
|
||||
url: string,
|
||||
body: B,
|
||||
options?: Omit<RequestOptions<B>, 'body'>
|
||||
) => request<R, B>('PUT', url, { ...options, body });
|
||||
19
front/contracts-frontend/src/utils/http/HttpError.ts
Normal file
19
front/contracts-frontend/src/utils/http/HttpError.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { ProblemDetails } from './ProblemDetails';
|
||||
|
||||
export class HttpError extends Error {
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly body: ProblemDetails | null;
|
||||
|
||||
constructor(
|
||||
status: number,
|
||||
statusText: string,
|
||||
body: ProblemDetails | null
|
||||
) {
|
||||
super(`${status} ${statusText}`);
|
||||
this.name = 'HttpError';
|
||||
this.status = status;
|
||||
this.statusText = statusText;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface ProblemDetails {
|
||||
type?: string;
|
||||
title?: string;
|
||||
status?: number;
|
||||
detail?: string;
|
||||
instance?: string;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export const maxLengthNonEmpty =
|
||||
(max: number) =>
|
||||
(value: unknown): true | string => {
|
||||
const str = (value ?? '').toString().trim();
|
||||
|
||||
if (!str) return 'Campo obligatorio';
|
||||
if (str.length > max)
|
||||
return `Debe tener como máximo ${max} caracteres`;
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
export const validateNifNie = (value: unknown): true | string => {
|
||||
if (typeof value !== 'string') return 'Formato no válido';
|
||||
|
||||
const sanitized = value.toUpperCase().replace(/[\s-]/g, '');
|
||||
const nifReg = /^[0-9]{8}[A-Z]$/;
|
||||
const nieReg = /^[XYZ][0-9]{7}[A-Z]$/;
|
||||
const letters = 'TRWAGMYFPDXBNJZSQVHLCKE';
|
||||
|
||||
let number: number;
|
||||
let control: string;
|
||||
|
||||
if (nifReg.test(sanitized)) {
|
||||
number = parseInt(sanitized.slice(0, 8), 10);
|
||||
control = sanitized[8];
|
||||
} else if (nieReg.test(sanitized)) {
|
||||
const prefix = { X: '0', Y: '1', Z: '2' }[sanitized[0] as 'X' | 'Y' | 'Z'];
|
||||
number = parseInt(prefix + sanitized.slice(1, 8), 10);
|
||||
control = sanitized[8];
|
||||
} else {
|
||||
return 'Formato de NIF/NIE inválido';
|
||||
}
|
||||
|
||||
const expected = letters[number % 23];
|
||||
return expected === control
|
||||
? true
|
||||
: 'Dígito de control incorrecto';
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
const parseToDate = (raw: string): Date | null => {
|
||||
const trimmed = raw.trim();
|
||||
let day: number, month: number, year: number;
|
||||
|
||||
const slashMatch = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(trimmed);
|
||||
if (slashMatch) {
|
||||
[, day, month, year] = slashMatch.map(Number);
|
||||
} else {
|
||||
const dashMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
|
||||
if (!dashMatch) return null;
|
||||
[, year, month, day] = dashMatch.map(Number);
|
||||
}
|
||||
|
||||
const date = new Date(year, month - 1, day);
|
||||
return date.getFullYear() === year &&
|
||||
date.getMonth() === month - 1 &&
|
||||
date.getDate() === day
|
||||
? date
|
||||
: null;
|
||||
};
|
||||
|
||||
export const minDate =
|
||||
(min: string) =>
|
||||
(value: unknown): true | string => {
|
||||
const reference = parseToDate(min);
|
||||
const dateVal = typeof value === 'string' ? parseToDate(value) : null;
|
||||
|
||||
if (!reference) throw new Error('minDate: formato de fecha incorrecto');
|
||||
if (!dateVal) return 'Fecha inválida';
|
||||
|
||||
return dateVal >= reference
|
||||
? true
|
||||
: `Debe ser igual o posterior a ${min}`;
|
||||
};
|
||||
|
||||
export const maxDate =
|
||||
(max: string) =>
|
||||
(value: unknown): true | string => {
|
||||
const reference = parseToDate(max);
|
||||
const dateVal = typeof value === 'string' ? parseToDate(value) : null;
|
||||
|
||||
if (!reference) throw new Error('maxDate: formato de fecha incorrecto');
|
||||
if (!dateVal) return 'Fecha inválida';
|
||||
|
||||
return dateVal <= reference
|
||||
? true
|
||||
: `Debe ser igual o anterior a ${max}`;
|
||||
};
|
||||
1
front/contracts-frontend/src/vite-env.d.ts
vendored
Normal file
1
front/contracts-frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
32
front/contracts-frontend/tsconfig.app.json
Normal file
32
front/contracts-frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"types": ["node"],
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
front/contracts-frontend/tsconfig.json
Normal file
7
front/contracts-frontend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
29
front/contracts-frontend/tsconfig.node.json
Normal file
29
front/contracts-frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
16
front/contracts-frontend/vite.config.ts
Normal file
16
front/contracts-frontend/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(),],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(root, 'src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user