Compare commits
1 Commits
main
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe2dd4ca6a |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -32,5 +32,5 @@
|
||||
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
||||
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
],
|
||||
"editor.inlineSuggest.showToolbar": "always"
|
||||
"editor.inlineSuggest.showToolbar": "onHover"
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
RUN pnpm add -g turbo
|
||||
RUN pnpm add -g turbo dotenv-cli
|
||||
COPY . .
|
||||
RUN turbo prune --scope=web --docker
|
||||
|
||||
@ -28,9 +28,8 @@ RUN pnpm install
|
||||
# Build the project
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
ARG URL_IUS_DIRECT
|
||||
ARG NEXT_PUBLIC_USE_DEV_COLORS
|
||||
RUN pnpm turbo run build --filter=web...
|
||||
COPY .env .env
|
||||
RUN pnpm dotenv -e .env turbo run build --filter=web...
|
||||
|
||||
FROM node:alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
@ -7,46 +7,42 @@ import wretch from 'wretch';
|
||||
const urls = getUrls();
|
||||
const api = wretch(urls.URL_IUS).options({ cache: 'no-store' }).errorType('json');
|
||||
|
||||
type Input = { cookie?: string; pageUrlParams: PageUrlParams; payload?: unknown };
|
||||
type Input = { pageUrlParams: PageUrlParams; payload?: unknown };
|
||||
|
||||
export async function getData({ pageUrlParams, cookie = '' }: Input) {
|
||||
export async function getData({ pageUrlParams }: Input) {
|
||||
const url = createUrl({
|
||||
...pageUrlParams,
|
||||
route: '',
|
||||
});
|
||||
|
||||
return api
|
||||
.headers({ cookie })
|
||||
.get(url)
|
||||
.res<t.ResponseGetData>((cb) => cb.json())
|
||||
.then((res) => res);
|
||||
}
|
||||
|
||||
export async function getMetaData({ pageUrlParams, cookie = '' }: Input) {
|
||||
export async function getMetaData({ pageUrlParams }: Input) {
|
||||
const url = createUrl({ ...pageUrlParams, route: '/meta' });
|
||||
|
||||
return api
|
||||
.headers({ cookie })
|
||||
.get(url)
|
||||
.res<t.ResponseMetaData>((res) => res.json())
|
||||
.then((res) => res);
|
||||
}
|
||||
|
||||
export async function getConfig({ pageUrlParams, cookie = '' }: Input) {
|
||||
export async function getConfig({ pageUrlParams }: Input) {
|
||||
const url = createUrl({ ...pageUrlParams, route: '/config' });
|
||||
|
||||
return api
|
||||
.headers({ cookie })
|
||||
.get(url)
|
||||
.res<t.ResponseConfig>((res) => res.json())
|
||||
.then((res) => res);
|
||||
}
|
||||
|
||||
export async function getConditions({ pageUrlParams, cookie = '' }: Input) {
|
||||
export async function getConditions({ pageUrlParams }: Input) {
|
||||
const url = createUrl({ ...pageUrlParams, route: '/conditions' });
|
||||
|
||||
return api
|
||||
.headers({ cookie })
|
||||
.get(url)
|
||||
.res<t.ResponseConditions>((res) => res.text())
|
||||
.then((res) => res);
|
||||
@ -59,7 +55,7 @@ export async function save({ pageUrlParams, payload }: Input) {
|
||||
.post(payload, url)
|
||||
.res<boolean>((res) => res.ok)
|
||||
.then((res) => res)
|
||||
.catch((error: WretchError) => error.json as t.HttpValidationError | t.HttpError);
|
||||
.catch((error: WretchError) => error.json as t.HttpValidationError);
|
||||
}
|
||||
|
||||
export async function retract({ pageUrlParams, payload }: Input) {
|
||||
@ -69,30 +65,28 @@ export async function retract({ pageUrlParams, payload }: Input) {
|
||||
.post(payload, url)
|
||||
.res<boolean>((res) => res.ok)
|
||||
.then((res) => res)
|
||||
.catch((error: WretchError) => error.json as t.HttpValidationError | t.HttpError);
|
||||
.catch((error: WretchError) => error.json as t.HttpValidationError);
|
||||
}
|
||||
|
||||
export async function getDocumentTypes({ pageUrlParams, cookie = '' }: Input) {
|
||||
export async function getDocumentTypes({ pageUrlParams }: Input) {
|
||||
const url = createUrl({ ...pageUrlParams, route: '/documenttypes' });
|
||||
|
||||
return api
|
||||
.headers({ cookie })
|
||||
.get(url)
|
||||
.res<t.ResponseDocumentTypes>((res) => res.json())
|
||||
.then((res) => res);
|
||||
}
|
||||
|
||||
export async function getDocuments({ pageUrlParams, cookie = '' }: Input) {
|
||||
export async function getDocuments({ pageUrlParams }: Input) {
|
||||
const url = createUrl({ ...pageUrlParams, route: '/documents' });
|
||||
|
||||
return api
|
||||
.headers({ cookie })
|
||||
.get(url)
|
||||
.res<t.ResponseDocuments>((res) => res.json())
|
||||
.then((res) => res);
|
||||
}
|
||||
|
||||
export function uploadDocument({
|
||||
export async function uploadDocument({
|
||||
pageUrlParams,
|
||||
document,
|
||||
formData,
|
||||
@ -106,15 +100,5 @@ export function uploadDocument({
|
||||
return fetch(urls.URL_IUS + url, {
|
||||
body: formData,
|
||||
method: 'POST',
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error as t.HttpError;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
import type { Document, DocumentType } from './types';
|
||||
|
||||
export function combineDocuments({
|
||||
documentTypes,
|
||||
documents,
|
||||
}: {
|
||||
documentTypes: DocumentType[];
|
||||
documents: Document[];
|
||||
}) {
|
||||
if (!documents.length) {
|
||||
return documentTypes.map((x) => ({ ...(x as Document), canUpload: true }));
|
||||
}
|
||||
|
||||
const nonUploadableDocuments = documents
|
||||
.filter(
|
||||
(document) =>
|
||||
!documentTypes.some(
|
||||
(documentType) => documentType.documentTypeId === document.documentTypeId
|
||||
)
|
||||
)
|
||||
.map((document) => ({ ...document, canUpload: false }));
|
||||
|
||||
return documentTypes
|
||||
.map((documentType) => {
|
||||
const targetDocument = documents.find(
|
||||
(document) => document.documentTypeId === documentType.documentTypeId
|
||||
);
|
||||
|
||||
return { ...documentType, ...targetDocument, canUpload: true };
|
||||
})
|
||||
.concat(nonUploadableDocuments);
|
||||
}
|
||||
|
||||
export type CombinedDocuments = ReturnType<typeof combineDocuments>;
|
||||
@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export type MetaObject = {
|
||||
disabled: boolean;
|
||||
fieldType: 'CHECKBOX' | 'DECIMAL' | 'INT' | 'STRING' | 'BIGSTRING';
|
||||
fieldType: 'CHECKBOX' | 'DECIMAL' | 'INT' | 'STRING' | 'TEXTAREA';
|
||||
label: string;
|
||||
max: number;
|
||||
min: number;
|
||||
@ -16,9 +16,11 @@ export type DocumentType = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Document = DocumentType & {
|
||||
documentId?: string;
|
||||
href?: string;
|
||||
export type Document = {
|
||||
documentId: string;
|
||||
documentTypeId: string;
|
||||
href: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ResponseGetData = Record<string, Value>;
|
||||
|
||||
@ -4,14 +4,10 @@ import type { PageProps } from '@/types/page';
|
||||
import { withError } from '@/utils/error';
|
||||
import { getPageUrlParams } from '@/utils/url';
|
||||
import type { Metadata } from 'next';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export async function generateMetadata(pageProps: PageProps): Promise<Metadata> {
|
||||
const pageUrlParams = getPageUrlParams(pageProps);
|
||||
const headersList = headers();
|
||||
const cookie = headersList.get('cookie') ?? '';
|
||||
|
||||
const { title } = await apiIUS.getConfig({ cookie, pageUrlParams });
|
||||
const { title } = await apiIUS.getConfig({ pageUrlParams });
|
||||
const text = `Условия: ${title} | Эволюция`;
|
||||
|
||||
return {
|
||||
@ -28,10 +24,7 @@ export default async function Page(pageProps: PageProps) {
|
||||
return withError({
|
||||
render: async () => {
|
||||
const pageUrlParams = getPageUrlParams(pageProps);
|
||||
const headersList = headers();
|
||||
const cookie = headersList.get('cookie') ?? '';
|
||||
|
||||
const conditions = await apiIUS.getConditions({ cookie, pageUrlParams });
|
||||
const conditions = await apiIUS.getConditions({ pageUrlParams });
|
||||
|
||||
return <Conditions html={conditions} />;
|
||||
},
|
||||
|
||||
@ -1,19 +1,13 @@
|
||||
import * as apiIUS from '@/api/ius/query';
|
||||
import { combineDocuments } from '@/api/ius/tools';
|
||||
import { Form } from '@/components/Form';
|
||||
import type { FormComponentProps } from '@/components/Form/types';
|
||||
import type { PageProps } from '@/types/page';
|
||||
import { withError } from '@/utils/error';
|
||||
import { getPageUrlParams } from '@/utils/url';
|
||||
import type { Metadata } from 'next';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export async function generateMetadata(pageProps: PageProps): Promise<Metadata> {
|
||||
const pageUrlParams = getPageUrlParams(pageProps);
|
||||
const headersList = headers();
|
||||
const cookie = headersList.get('cookie') ?? '';
|
||||
|
||||
const { title } = await apiIUS.getConfig({ cookie, pageUrlParams });
|
||||
const { title } = await apiIUS.getConfig({ pageUrlParams });
|
||||
const text = `${title} | Эволюция`;
|
||||
|
||||
return {
|
||||
@ -31,24 +25,14 @@ export default async function Page(pageProps: PageProps) {
|
||||
render: async () => {
|
||||
const pageUrlParams = getPageUrlParams(pageProps);
|
||||
|
||||
const headersList = headers();
|
||||
const cookie = headersList.get('cookie') ?? '';
|
||||
|
||||
return Promise.all([
|
||||
apiIUS.getData({ cookie, pageUrlParams }),
|
||||
apiIUS.getMetaData({ cookie, pageUrlParams }),
|
||||
apiIUS.getConfig({ cookie, pageUrlParams }),
|
||||
apiIUS.getDocumentTypes({ cookie, pageUrlParams }),
|
||||
apiIUS.getDocuments({ cookie, pageUrlParams }),
|
||||
apiIUS.getData({ pageUrlParams }),
|
||||
apiIUS.getMetaData({ pageUrlParams }),
|
||||
apiIUS.getConfig({ pageUrlParams }),
|
||||
apiIUS.getDocumentTypes({ pageUrlParams }),
|
||||
apiIUS.getDocuments({ pageUrlParams }),
|
||||
]).then(([data, metaData, { title }, documentTypes, documents]) => {
|
||||
const combinedDocuments = combineDocuments({ documentTypes, documents });
|
||||
const props: FormComponentProps = {
|
||||
combinedDocuments,
|
||||
data,
|
||||
metaData,
|
||||
pageUrlParams,
|
||||
title,
|
||||
};
|
||||
const props = { data, documentTypes, documents, metaData, pageUrlParams, title };
|
||||
|
||||
return <Form {...props} />;
|
||||
});
|
||||
|
||||
@ -46,7 +46,6 @@ export default function RootLayout({ children }: { readonly children: React.Reac
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" crossOrigin="use-credentials" />
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
<Header>
|
||||
|
||||
25
apps/web/app/manifest.ts
Normal file
25
apps/web/app/manifest.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
background_color: '#fff',
|
||||
description: 'External | Эволюция',
|
||||
display: 'standalone',
|
||||
icons: [
|
||||
{
|
||||
sizes: '192x192',
|
||||
src: '/android-chrome-192x192.png',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
sizes: '512x512',
|
||||
src: '/android-chrome-512x512.png',
|
||||
type: 'image/png',
|
||||
},
|
||||
],
|
||||
name: 'External | Эволюция',
|
||||
short_name: 'External | Эволюция',
|
||||
start_url: '/',
|
||||
theme_color: '#fff',
|
||||
};
|
||||
}
|
||||
99
apps/web/components/Form/Buttons.tsx
Normal file
99
apps/web/components/Form/Buttons.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
/* eslint-disable react/jsx-curly-newline */
|
||||
/* eslint-disable no-negated-condition */
|
||||
import { FormContext } from './context/form-context';
|
||||
import * as apiIus from '@/api/ius/query';
|
||||
import { useFormStore } from '@/store/ius/form';
|
||||
import { Button } from '@repo/ui';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { pick } from 'radash';
|
||||
import { useCallback } from 'react';
|
||||
import { useContext } from 'react';
|
||||
|
||||
const ERROR_UPLOAD_DOCUMENT = 'Произошла ошибка при загрузке документов';
|
||||
|
||||
export function Buttons() {
|
||||
const { reset, resetValidation, setValidation, values } = useFormStore();
|
||||
const { formFiles, pageUrlParams, setFormState } = useContext(FormContext);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
apiIus.save({ pageUrlParams, payload: values }).then((res) => {
|
||||
if (typeof res !== 'boolean') {
|
||||
setTimeout(() => {
|
||||
setFormState({ status: 'edit' });
|
||||
}, 300);
|
||||
Object.keys(res.errors).forEach((name) => {
|
||||
const elementValidation = res?.errors?.[name];
|
||||
if (elementValidation)
|
||||
setValidation({ message: elementValidation[0] ?? '', name, valid: false });
|
||||
});
|
||||
} else {
|
||||
setFormState({ status: 'success' });
|
||||
setTimeout(() => {
|
||||
router.refresh();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}, [pageUrlParams, router, setFormState, setValidation, values]);
|
||||
|
||||
const handleUploadFiles = useCallback(() => {
|
||||
setFormState({ status: 'pending' });
|
||||
resetValidation();
|
||||
const uploadFiles = formFiles.map((formFile) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', formFile.file);
|
||||
const document = pick(formFile, ['documentTypeId']);
|
||||
|
||||
return apiIus.uploadDocument({
|
||||
document,
|
||||
formData,
|
||||
pageUrlParams,
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.allSettled(uploadFiles).catch(() => {
|
||||
setFormState({ status: 'error', text: ERROR_UPLOAD_DOCUMENT });
|
||||
throw new Error(ERROR_UPLOAD_DOCUMENT);
|
||||
});
|
||||
}, [formFiles, pageUrlParams, resetValidation, setFormState]);
|
||||
|
||||
const handleRetract = useCallback(() => {
|
||||
setFormState({ status: 'pending' });
|
||||
resetValidation();
|
||||
apiIus.retract({ pageUrlParams, payload: values }).then((res) => {
|
||||
if (typeof res !== 'boolean') {
|
||||
setTimeout(() => {
|
||||
setFormState({ status: 'edit' });
|
||||
}, 300);
|
||||
Object.keys(res.errors).forEach((name) => {
|
||||
const elementValidation = res?.errors?.[name];
|
||||
if (elementValidation)
|
||||
setValidation({ message: elementValidation[0] ?? '', name, valid: false });
|
||||
});
|
||||
} else {
|
||||
setFormState({ status: 'success' });
|
||||
setTimeout(() => {
|
||||
router.refresh();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}, [pageUrlParams, resetValidation, router, setFormState, setValidation, values]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-2 gap-x-4 md:grid-cols-3">
|
||||
<Button
|
||||
intent="outline-danger"
|
||||
onClick={() => {
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button intent="outline-secondary" onClick={() => handleRetract()}>
|
||||
Возврат на доработку
|
||||
</Button>
|
||||
<Button onClick={() => handleUploadFiles().then(() => handleSave())}>Сохранить</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
/* eslint-disable react/jsx-curly-newline */
|
||||
/* eslint-disable no-negated-condition */
|
||||
import { FormContext } from '../context/form-context';
|
||||
import * as apiIus from '@/api/ius/query';
|
||||
import { useFormStore } from '@/store/ius/form';
|
||||
import { Button } from '@repo/ui';
|
||||
import { useCallback, useContext } from 'react';
|
||||
|
||||
const ERROR_RETRACT = 'Произошла ошибка при возврате на доработку';
|
||||
const ERROR_SAVE = 'Произошла ошибка при сохранении';
|
||||
|
||||
export function Buttons() {
|
||||
const { reset, resetValidation, setValidation, status, values } = useFormStore();
|
||||
const { pageUrlParams, setFormState } = useContext(FormContext);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
setFormState({ status: 'pending' });
|
||||
resetValidation();
|
||||
apiIus.save({ pageUrlParams, payload: values }).then((res) => {
|
||||
if (typeof res !== 'boolean') {
|
||||
const { errors } = res;
|
||||
|
||||
if (Array.isArray(errors)) {
|
||||
setFormState({ status: 'error', text: errors?.at(0) || ERROR_SAVE });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(errors).forEach((name) => {
|
||||
const elementValidation = errors?.[name];
|
||||
if (elementValidation)
|
||||
setValidation({ message: elementValidation[0] ?? '', name, valid: false });
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
setFormState({ status: 'edit' });
|
||||
}, 300);
|
||||
} else {
|
||||
setFormState({ status: 'success' });
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}, [pageUrlParams, resetValidation, setFormState, setValidation, values]);
|
||||
|
||||
const handleRetract = useCallback(() => {
|
||||
setFormState({ status: 'pending' });
|
||||
resetValidation();
|
||||
apiIus.retract({ pageUrlParams, payload: values }).then((res) => {
|
||||
if (typeof res !== 'boolean') {
|
||||
const { errors } = res;
|
||||
|
||||
if (Array.isArray(errors)) {
|
||||
setFormState({ status: 'error', text: errors?.at(0) || ERROR_RETRACT });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setFormState({ status: 'edit' });
|
||||
}, 300);
|
||||
Object.keys(errors).forEach((name) => {
|
||||
const elementValidation = errors?.[name];
|
||||
if (elementValidation)
|
||||
setValidation({ message: elementValidation[0] ?? '', name, valid: false });
|
||||
});
|
||||
} else {
|
||||
setFormState({ status: 'success' });
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}, [pageUrlParams, resetValidation, setFormState, setValidation, values]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-2 gap-x-4 md:grid-cols-3">
|
||||
<Button
|
||||
intent="outline-danger"
|
||||
onClick={() => {
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button intent="outline-secondary" onClick={handleRetract}>
|
||||
Возврат на доработку
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={status !== 'edited'}>
|
||||
Сохранить
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export * from './Buttons';
|
||||
export * from './Elements';
|
||||
@ -1,76 +0,0 @@
|
||||
/* eslint-disable react/jsx-curly-newline */
|
||||
/* eslint-disable no-negated-condition */
|
||||
import { FormContext } from '../context/form-context';
|
||||
import * as apiIus from '@/api/ius/query';
|
||||
import type { HttpError } from '@/api/ius/types';
|
||||
import { useFormStore } from '@/store/ius/form';
|
||||
import { Button } from '@repo/ui';
|
||||
import { pick } from 'radash';
|
||||
import { useCallback, useContext } from 'react';
|
||||
|
||||
const ERROR_UPLOAD_DOCUMENT = 'Произошла ошибка при загрузке документов';
|
||||
const SUCCESS_UPLOAD_DOCUMENTS = 'Файлы успешно загружены';
|
||||
|
||||
export function Buttons() {
|
||||
const { resetValidation } = useFormStore();
|
||||
const { formFiles, pageUrlParams, setFormFiles, setFormState } = useContext(FormContext);
|
||||
|
||||
const handleUploadFiles = useCallback(() => {
|
||||
setFormState({ status: 'pending' });
|
||||
resetValidation();
|
||||
const uploadFiles = formFiles.map((formFile) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', formFile.file);
|
||||
const document = pick(formFile, ['documentTypeId']);
|
||||
|
||||
return apiIus.uploadDocument({
|
||||
document,
|
||||
formData,
|
||||
pageUrlParams,
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(uploadFiles)
|
||||
.then(async (res) => {
|
||||
const errors = res.filter((x) => typeof x !== 'boolean') as HttpError[];
|
||||
|
||||
if (!errors.length) {
|
||||
setFormState({
|
||||
status: 'success',
|
||||
text: SUCCESS_UPLOAD_DOCUMENTS,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const error = errors.find((x) => x.errors.length)?.errors.at(0) || ERROR_UPLOAD_DOCUMENT;
|
||||
|
||||
setFormState({ status: 'error', text: error });
|
||||
})
|
||||
.catch((error) => {
|
||||
setFormState({
|
||||
status: 'error',
|
||||
text: error ? JSON.stringify(error) : ERROR_UPLOAD_DOCUMENT,
|
||||
});
|
||||
});
|
||||
}, [formFiles, pageUrlParams, resetValidation, setFormState]);
|
||||
|
||||
const handleCancel = () => {
|
||||
setFormFiles([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-2 gap-x-4 md:grid-cols-3">
|
||||
<Button intent="outline-danger" onClick={handleCancel} disabled={!formFiles.length}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button onClick={handleUploadFiles} disabled={!formFiles.length}>
|
||||
Загрузить файлы
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export * from './Buttons';
|
||||
export * from './Files';
|
||||
@ -1,9 +1,9 @@
|
||||
import type { FormComponentProps } from '../types';
|
||||
import type { Props } from './types';
|
||||
import type { MetaObject } from '@/api/ius/types';
|
||||
import { mapFieldTypeElement } from '@/config/elements';
|
||||
import { useFormStore } from '@/store/ius/form';
|
||||
import { ElementContainer } from '@repo/ui';
|
||||
import { get } from 'radash';
|
||||
import { get, omit } from 'radash';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
function RenderElement({
|
||||
@ -31,9 +31,9 @@ function RenderElement({
|
||||
>
|
||||
<Element
|
||||
loading={!Object.keys(values).length}
|
||||
checked={fieldType === 'CHECKBOX' ? Boolean(values[name]) ?? false : false}
|
||||
checked={fieldType === 'CHECKBOX' ? Boolean(values[name]) || false : false}
|
||||
id={name}
|
||||
value={values[name] ?? ''}
|
||||
value={values[name] || ''}
|
||||
min={min}
|
||||
max={max}
|
||||
onChange={(e) => {
|
||||
@ -49,31 +49,21 @@ function RenderElement({
|
||||
);
|
||||
}
|
||||
|
||||
export function Elements({ data, metaData }: FormComponentProps) {
|
||||
export function Elements({ data, metaData }: Props) {
|
||||
const { init } = useFormStore();
|
||||
|
||||
useEffect(() => {
|
||||
init(data);
|
||||
}, [data, init]);
|
||||
|
||||
const defaultElements = Object.keys(metaData).filter(
|
||||
(x) => metaData[x]?.fieldType !== 'BIGSTRING'
|
||||
);
|
||||
|
||||
const bigStringElements = Object.keys(metaData).filter(
|
||||
(x) => metaData[x]?.fieldType === 'BIGSTRING'
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-2 grid gap-2 gap-x-4 md:grid md:grid-cols-2 lg:grid-cols-3">
|
||||
{defaultElements.map((name) => (
|
||||
{(Object.keys(omit(metaData, ['comment'])) as Array<keyof MetaObject>).map((name) => (
|
||||
<RenderElement key={name} {...get(metaData, name)} name={name} />
|
||||
))}
|
||||
</div>
|
||||
{bigStringElements.map((name) => (
|
||||
<RenderElement key={name} {...get(metaData, name)} name={name} fieldType="BIGSTRING" />
|
||||
))}
|
||||
<RenderElement {...get(metaData, 'comment')} fieldType="TEXTAREA" name="comment" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,16 +1,17 @@
|
||||
import { FormContext } from '../context/form-context';
|
||||
import type { FormComponentProps } from '../types';
|
||||
import { FormContext } from './context/form-context';
|
||||
import type { Props } from './types';
|
||||
import type * as IUS from '@/api/ius/types';
|
||||
import { ArrowDownTrayIcon } from '@heroicons/react/24/solid';
|
||||
import { Heading, InputFile } from '@repo/ui';
|
||||
import Link from 'next/link';
|
||||
import { useContext, useEffect, useRef } from 'react';
|
||||
import { useContext } from 'react';
|
||||
|
||||
type DownloadDocumentProps = Pick<FileProps, 'document'>;
|
||||
|
||||
function DownloadDocument({ document }: DownloadDocumentProps) {
|
||||
return document?.href ? (
|
||||
return document ? (
|
||||
<Link
|
||||
href={'/api/ius' + document.href}
|
||||
href={'/api/ius' + document?.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-600 h-10 px-5 py-2.5 text-sm font-medium hover:underline"
|
||||
@ -26,14 +27,14 @@ function DownloadDocument({ document }: DownloadDocumentProps) {
|
||||
}
|
||||
|
||||
type FileProps = {
|
||||
readonly document: FormComponentProps['combinedDocuments'][number];
|
||||
readonly document: IUS.Document | undefined;
|
||||
readonly documentType: IUS.DocumentType;
|
||||
};
|
||||
|
||||
function File({ document }: FileProps) {
|
||||
function File({ document, documentType }: FileProps) {
|
||||
const { documentTypeId, name } = documentType;
|
||||
const { formFiles, setFormFiles } = useContext(FormContext);
|
||||
|
||||
const { canUpload, documentTypeId, name } = document;
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files !== null) {
|
||||
const file = event.target.files.item(0);
|
||||
@ -45,29 +46,26 @@ function File({ document }: FileProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const uploadableFile = formFiles.some((x) => x.documentTypeId === document.documentTypeId);
|
||||
if (!uploadableFile && inputRef.current) inputRef.current.value = '';
|
||||
}, [document.documentTypeId, formFiles]);
|
||||
|
||||
return (
|
||||
<div key={documentTypeId} className="flex flex-col gap-1">
|
||||
<label className="mb-2 block text-sm font-normal text-gray-900">{name}:</label>
|
||||
<InputFile ref={inputRef} onChange={handleFileChange} disabled={!canUpload} />
|
||||
<InputFile onChange={handleFileChange} />
|
||||
<DownloadDocument document={document} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Files({ combinedDocuments }: FormComponentProps) {
|
||||
export function Files({ documentTypes, documents }: Props) {
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<Heading className="text-sm">Документы</Heading>
|
||||
<div className="grid gap-2 md:grid-cols-2">
|
||||
{combinedDocuments.map((document) => (
|
||||
<File key={document.documentTypeId} document={document} />
|
||||
<Heading className="text-sms">Документы</Heading>
|
||||
<div className="flex flex-col gap-2">
|
||||
{documentTypes.map((documentType) => (
|
||||
<File
|
||||
key={documentType.documentTypeId}
|
||||
documentType={documentType}
|
||||
document={documents.find((x) => x.documentTypeId === documentType.documentTypeId)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -1,61 +1,44 @@
|
||||
'use client';
|
||||
import { FormContext } from './context/form-context';
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/solid';
|
||||
import { Background, Button, LoadingSpinner } from '@repo/ui';
|
||||
import { Background, LoadingSpinner } from '@repo/ui';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useContext } from 'react';
|
||||
|
||||
function OverlayWrapper({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<Background className="absolute left-0 top-0 grid h-full w-full place-items-center bg-opacity-80 backdrop-blur-sm">
|
||||
<div className="absolute bottom-[50vh] flex flex-col items-center gap-2 md:relative md:bottom-0">
|
||||
<div className="absolute bottom-[50vh] flex flex-row items-center gap-2 md:relative md:bottom-0">
|
||||
{children}
|
||||
</div>
|
||||
</Background>
|
||||
);
|
||||
}
|
||||
|
||||
function StateContentWrapper({ children }: PropsWithChildren) {
|
||||
return <div className="flex flex-row items-center gap-2">{children}</div>;
|
||||
}
|
||||
|
||||
export function Overlay() {
|
||||
const { formState, setFormState } = useContext(FormContext);
|
||||
const { formState } = useContext(FormContext);
|
||||
const { status, text } = formState;
|
||||
|
||||
let stateContent: JSX.Element | false = false;
|
||||
|
||||
if (status === 'pending') {
|
||||
stateContent = (
|
||||
<StateContentWrapper>
|
||||
if (status === 'pending')
|
||||
return (
|
||||
<OverlayWrapper>
|
||||
{LoadingSpinner} <p className="font-medium">Загрузка...</p>
|
||||
</StateContentWrapper>
|
||||
</OverlayWrapper>
|
||||
);
|
||||
}
|
||||
if (status === 'success') {
|
||||
stateContent = (
|
||||
<StateContentWrapper>
|
||||
<CheckCircleIcon className="h-10 w-10 fill-green-500" title="OK" />
|
||||
if (status === 'success')
|
||||
return (
|
||||
<OverlayWrapper>
|
||||
<CheckCircleIcon className="h-10 w-10 fill-green-500" title="OK" />{' '}
|
||||
<p className="font-medium">Данные сохранены</p>
|
||||
</StateContentWrapper>
|
||||
</OverlayWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
stateContent = (
|
||||
<>
|
||||
<StateContentWrapper>
|
||||
<XCircleIcon className="h-10 w-10 fill-red-500" title="Error" />
|
||||
<p className="font-medium">{text}</p>
|
||||
</StateContentWrapper>{' '}
|
||||
<Button type="button" intent="text" onClick={() => setFormState({ status: 'edit' })}>
|
||||
Закрыть
|
||||
</Button>
|
||||
</>
|
||||
return (
|
||||
<OverlayWrapper>
|
||||
<XCircleIcon className="h-10 w-10 fill-red-500" title="Error" />{' '}
|
||||
<p className="font-medium">{text}</p>
|
||||
</OverlayWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (!stateContent) return false;
|
||||
|
||||
return <OverlayWrapper>{stateContent}</OverlayWrapper>;
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
'use client';
|
||||
import * as Common from './Common';
|
||||
import { Buttons } from './Buttons';
|
||||
import { FormContext, FormContextProvider } from './context/form-context';
|
||||
import * as Documents from './Documents';
|
||||
import { Elements } from './Elements';
|
||||
import { Files } from './Files';
|
||||
import { Header } from './Header';
|
||||
import { Overlay } from './Overlay';
|
||||
import type { FormComponentProps } from './types';
|
||||
import type { Props } from './types';
|
||||
import { createUrl } from '@/utils/url';
|
||||
import { Background, Divider } from '@repo/ui';
|
||||
import type { FC } from 'react';
|
||||
import { useContext } from 'react';
|
||||
|
||||
function Content(props: FormComponentProps) {
|
||||
function Content(props: Props) {
|
||||
const { title } = props;
|
||||
const { pageUrlParams } = useContext(FormContext);
|
||||
|
||||
@ -18,17 +19,16 @@ function Content(props: FormComponentProps) {
|
||||
<Background className="relative flex w-full flex-col gap-2 p-5">
|
||||
<Overlay />
|
||||
<Header title={title} link={'/ius' + createUrl({ ...pageUrlParams, route: '/conditions' })} />
|
||||
<Common.Elements {...props} />
|
||||
<Common.Buttons />
|
||||
<Elements {...props} />
|
||||
<Divider />
|
||||
<Documents.Files {...props} />
|
||||
<Documents.Buttons />
|
||||
<Files {...props} />
|
||||
<Divider />
|
||||
<Buttons />
|
||||
</Background>
|
||||
);
|
||||
}
|
||||
|
||||
function withContext<T extends FormComponentProps>(Component: FC<T>) {
|
||||
function withContext<T extends Props>(Component: FC<T>) {
|
||||
return (props: T) => {
|
||||
const { pageUrlParams } = props;
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import type { CombinedDocuments } from '@/api/ius/tools';
|
||||
import type * as IUS from '@/api/ius/types';
|
||||
import type { PageUrlParams } from '@/utils/url';
|
||||
|
||||
export type FormComponentProps = {
|
||||
readonly combinedDocuments: CombinedDocuments;
|
||||
export type Props = {
|
||||
readonly data: IUS.ResponseGetData;
|
||||
readonly documentTypes: IUS.ResponseDocumentTypes;
|
||||
readonly documents: IUS.ResponseDocuments;
|
||||
readonly metaData: IUS.ResponseMetaData;
|
||||
readonly pageUrlParams: PageUrlParams;
|
||||
readonly title: string;
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import { getClientEnv } from '@/config/env';
|
||||
import { getEnv } from '@/config/env';
|
||||
import Image from 'next/image';
|
||||
import logo from 'public/assets/images/logo-primary.svg';
|
||||
import logoDev from 'public/assets/images/logo-primary-dev.svg';
|
||||
|
||||
const env = getClientEnv();
|
||||
const env = getEnv();
|
||||
|
||||
export function Logo() {
|
||||
return (
|
||||
<Image priority alt="logo" src={env.NEXT_PUBLIC_USE_DEV_COLORS ? logoDev : logo} height="24" />
|
||||
);
|
||||
return <Image priority alt="logo" src={env.USE_DEV_COLORS ? logoDev : logo} height="24" />;
|
||||
}
|
||||
|
||||
@ -6,9 +6,9 @@ function wrapMap<C, T extends Record<MetaObject['fieldType'], C>>(arg: T) {
|
||||
}
|
||||
|
||||
export const mapFieldTypeElement = wrapMap({
|
||||
BIGSTRING: Textarea,
|
||||
CHECKBOX: Checkbox,
|
||||
DECIMAL: InputNumber,
|
||||
INT: InputNumber,
|
||||
STRING: Input,
|
||||
TEXTAREA: Textarea,
|
||||
});
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
const { clientEnvSchema, serverEnvSchema } = require('./schema/env');
|
||||
const envSchema = require('./schema/env.js');
|
||||
|
||||
const getClientEnv = () => clientEnvSchema.parse(process.env);
|
||||
const getServerEnv = () => serverEnvSchema.parse(process.env);
|
||||
const getEnv = () => envSchema.parse(process.env);
|
||||
|
||||
module.exports = { getClientEnv, getServerEnv };
|
||||
module.exports = { getEnv };
|
||||
|
||||
@ -2,15 +2,7 @@ const { z } = require('zod');
|
||||
|
||||
const envSchema = z.object({
|
||||
URL_IUS_DIRECT: z.string(),
|
||||
NEXT_PUBLIC_USE_DEV_COLORS: z.string().optional(),
|
||||
USE_DEV_COLORS: z.string().optional(),
|
||||
});
|
||||
|
||||
const serverEnvSchema = envSchema.pick({
|
||||
URL_IUS_DIRECT: true,
|
||||
});
|
||||
|
||||
const clientEnvSchema = envSchema.pick({
|
||||
NEXT_PUBLIC_USE_DEV_COLORS: true,
|
||||
});
|
||||
|
||||
module.exports = { envSchema, serverEnvSchema, clientEnvSchema };
|
||||
module.exports = envSchema;
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { getServerEnv } from './env';
|
||||
import { getEnv } from './env';
|
||||
import proxyUrls from '@/constants/urls';
|
||||
import { isServer } from '@/utils/common';
|
||||
|
||||
export function getUrls() {
|
||||
if (isServer()) {
|
||||
const env = getServerEnv();
|
||||
const env = getEnv();
|
||||
const { URL_IUS_DIRECT } = env;
|
||||
|
||||
return {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { getClientEnv } from '../config/env';
|
||||
import { getEnv } from '../config/env';
|
||||
|
||||
export const COLORS_PROD = {
|
||||
COLOR_DANGER: '#B20004',
|
||||
@ -12,5 +12,5 @@ export const COLORS_DEV = {
|
||||
COLOR_SECONDARY: '#c54a84',
|
||||
COLOR_TERTIARTY: '#FF9112',
|
||||
};
|
||||
const env = getClientEnv();
|
||||
export const COLORS = env.NEXT_PUBLIC_USE_DEV_COLORS ? COLORS_DEV : COLORS_PROD;
|
||||
const env = getEnv();
|
||||
export const COLORS = env.USE_DEV_COLORS ? COLORS_DEV : COLORS_PROD;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { envSchema } = require('./config/schema/env');
|
||||
const envSchema = require('./config/schema/env');
|
||||
const urls = require('./constants/urls');
|
||||
|
||||
const env = envSchema.parse(process.env);
|
||||
|
||||
const favicons = fs.readdirSync('./public/favicon/prod');
|
||||
const faviconSubPath = env.NEXT_PUBLIC_USE_DEV_COLORS ? '/favicon/dev' : '/favicon/prod';
|
||||
const faviconSubPath = env.USE_DEV_COLORS ? '/favicon/dev' : '/favicon/prod';
|
||||
function buildFaviconRewrite(source) {
|
||||
return {
|
||||
destination: String.prototype.concat(faviconSubPath, source),
|
||||
@ -34,7 +34,6 @@ const nextConfig = {
|
||||
...favicons.map((fileName) => buildFaviconRewrite(`/${fileName}`)),
|
||||
];
|
||||
},
|
||||
env,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
@ -11,14 +11,14 @@
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@repo/tsconfig": "workspace:*",
|
||||
"@repo/ui": "workspace:*",
|
||||
"next": "^14.1.0",
|
||||
"next": "^14.0.3",
|
||||
"radash": "^11.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"@repo/ui": "workspace:*",
|
||||
"wretch": "^2.7.0",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.5.0"
|
||||
"zustand": "^4.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.6",
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
{
|
||||
"background_color": "#fff",
|
||||
"description": "External | Эволюция",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"sizes": "192x192",
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"sizes": "512x512",
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"name": "External | Эволюция",
|
||||
"short_name": "External | Эволюция",
|
||||
"start_url": "/",
|
||||
"theme_color": "#fff"
|
||||
}
|
||||
@ -15,7 +15,6 @@ type FormState = {
|
||||
resetValidation: () => void;
|
||||
setValidation: (input: { name: string } & ElementValidation) => void;
|
||||
setValue: ({ name, value }: { name: string; value: Values[number] }) => void;
|
||||
status?: 'init' | 'edited';
|
||||
validation: Record<string, ElementValidation | undefined>;
|
||||
values: Values;
|
||||
};
|
||||
@ -25,12 +24,10 @@ export const useFormStore = create<FormState>((set) => ({
|
||||
init: (values) =>
|
||||
set(() => ({
|
||||
defaultValues: values,
|
||||
status: 'init',
|
||||
values,
|
||||
})),
|
||||
reset: () =>
|
||||
set((state) => ({
|
||||
status: 'init',
|
||||
validation: {},
|
||||
values: state.defaultValues,
|
||||
})),
|
||||
@ -47,7 +44,6 @@ export const useFormStore = create<FormState>((set) => ({
|
||||
})),
|
||||
setValue: ({ name, value }) =>
|
||||
set((state) => ({
|
||||
status: 'edited',
|
||||
validation: {
|
||||
...state.validation,
|
||||
[name]: undefined,
|
||||
|
||||
@ -1,20 +1,12 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
external_client:
|
||||
ports:
|
||||
- '3000:3000'
|
||||
environment:
|
||||
- URL_IUS_DIRECT=${URL_IUS_DIRECT}
|
||||
- NEXT_PUBLIC_USE_DEV_COLORS=${NEXT_PUBLIC_USE_DEV_COLORS}
|
||||
- USE_DEV_COLORS=${USE_DEV_COLORS}
|
||||
build:
|
||||
args:
|
||||
- URL_IUS_DIRECT=${URL_IUS_DIRECT}
|
||||
- NEXT_PUBLIC_USE_DEV_COLORS=${NEXT_PUBLIC_USE_DEV_COLORS}
|
||||
context: .
|
||||
dockerfile: ./apps/web/Dockerfile
|
||||
networks:
|
||||
- external_network
|
||||
restart: always
|
||||
|
||||
networks:
|
||||
external_network:
|
||||
external:
|
||||
name: external_network
|
||||
|
||||
@ -22,7 +22,6 @@ const variants = cva(
|
||||
'border border-primary text-primary-500 hover:bg-primary-500 hover:text-white',
|
||||
secondary:
|
||||
'bg-primary-50 text-primary-500 border border-transparent hover:bg-primary-100 hover:text-primary-600',
|
||||
text: 'bg-none text-primary-500 hover:bg-primary-50',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ export const InputFile = forwardRef<HTMLInputElement, InputProps>((props, ref) =
|
||||
type="file"
|
||||
className="file:bg-primary-50 file:text-primary-500 hover:file:bg-primary-100 block w-full text-sm
|
||||
text-slate-500 file:mr-4 file:rounded-sm file:border-0
|
||||
file:px-4 file:py-2 file:text-sm file:font-semibold
|
||||
hover:file:cursor-pointer disabled:cursor-not-allowed disabled:opacity-30 disabled:file:cursor-not-allowed"
|
||||
file:px-4 file:py-2 file:text-sm
|
||||
file:font-semibold hover:file:cursor-pointer"
|
||||
/>
|
||||
));
|
||||
|
||||
1233
pnpm-lock.yaml
generated
1233
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user