Merge pull request #2 from ma1andes/add_filters
Generate filtering/sorting and searchable dropdowns
This commit is contained in:
39
client/package-lock.json
generated
39
client/package-lock.json
generated
@@ -1449,9 +1449,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1466,9 +1463,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1483,9 +1477,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1500,9 +1491,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1517,9 +1505,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1534,9 +1519,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1551,9 +1533,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1568,9 +1547,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1585,9 +1561,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1602,9 +1575,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1619,9 +1589,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1636,9 +1603,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1653,9 +1617,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,22 @@ const httpClient = async (url: string, options: fetchUtils.Options = {}) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function buildQueryString(query: Record<string, unknown>) {
|
||||||
|
const search = new URLSearchParams();
|
||||||
|
Object.entries(query).forEach(([key, val]) => {
|
||||||
|
if (val === undefined || val === null || val === '') return;
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
val.forEach((v) => {
|
||||||
|
if (v === undefined || v === null || v === '') return;
|
||||||
|
search.append(key, String(v));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
search.set(key, String(val));
|
||||||
|
});
|
||||||
|
return search.toString();
|
||||||
|
}
|
||||||
|
|
||||||
const dataProvider: DataProvider = {
|
const dataProvider: DataProvider = {
|
||||||
getList: async (resource, params) => {
|
getList: async (resource, params) => {
|
||||||
const { page, perPage } = params.pagination!;
|
const { page, perPage } = params.pagination!;
|
||||||
@@ -22,23 +38,15 @@ const dataProvider: DataProvider = {
|
|||||||
const start = (page - 1) * perPage;
|
const start = (page - 1) * perPage;
|
||||||
const end = page * perPage;
|
const end = page * perPage;
|
||||||
|
|
||||||
const query: Record<string, string> = {
|
const query: Record<string, unknown> = {
|
||||||
_start: String(start),
|
_start: start,
|
||||||
_end: String(end),
|
_end: end,
|
||||||
_sort: field,
|
_sort: field,
|
||||||
_order: order,
|
_order: order,
|
||||||
|
...(params.filter ?? {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (params.filter) {
|
const queryString = buildQueryString(query);
|
||||||
Object.keys(params.filter).forEach((key) => {
|
|
||||||
const val = params.filter[key];
|
|
||||||
if (val !== undefined && val !== null && val !== '') {
|
|
||||||
query[key] = String(val);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryString = new URLSearchParams(query).toString();
|
|
||||||
const url = `${apiUrl}/${resource}?${queryString}`;
|
const url = `${apiUrl}/${resource}?${queryString}`;
|
||||||
const { json, headers } = await httpClient(url);
|
const { json, headers } = await httpClient(url);
|
||||||
|
|
||||||
@@ -67,15 +75,16 @@ const dataProvider: DataProvider = {
|
|||||||
const start = (page - 1) * perPage;
|
const start = (page - 1) * perPage;
|
||||||
const end = page * perPage;
|
const end = page * perPage;
|
||||||
|
|
||||||
const query: Record<string, string> = {
|
const query: Record<string, unknown> = {
|
||||||
_start: String(start),
|
_start: start,
|
||||||
_end: String(end),
|
_end: end,
|
||||||
_sort: field,
|
_sort: field,
|
||||||
_order: order,
|
_order: order,
|
||||||
[params.target]: String(params.id),
|
[params.target]: params.id,
|
||||||
|
...(params.filter ?? {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryString = new URLSearchParams(query).toString();
|
const queryString = buildQueryString(query);
|
||||||
const url = `${apiUrl}/${resource}?${queryString}`;
|
const url = `${apiUrl}/${resource}?${queryString}`;
|
||||||
const { json, headers } = await httpClient(url);
|
const { json, headers } = await httpClient(url);
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Create, SimpleForm, TextInput, NumberInput } from 'react-admin';
|
import { Create, SimpleForm, TextInput, NumberInput } from 'react-admin';
|
||||||
|
|
||||||
|
|
||||||
export const EquipmentTypeCreate = () => (
|
export const EquipmentTypeCreate = () => (
|
||||||
<Create>
|
<Create>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<TextInput source="code" label="Код" isRequired />
|
<TextInput source="code" label="Код вида оборудования" isRequired />
|
||||||
<TextInput source="name" label="Наименование" isRequired />
|
<TextInput source="name" label="Наименование вида" isRequired />
|
||||||
<TextInput source="manufacturer" label="Производитель" />
|
<TextInput source="manufacturer" label="Производитель" />
|
||||||
<NumberInput source="maintenanceIntervalHours" label="Периодичность ТО (ч)" />
|
<NumberInput source="maintenanceIntervalHours" label="Периодичность ТО, моточасов" />
|
||||||
<NumberInput source="overhaulIntervalHours" label="Периодичность КР (ч)" />
|
<NumberInput source="overhaulIntervalHours" label="Периодичность КР, моточасов" />
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</Create>
|
</Create>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Edit, SimpleForm, TextInput, NumberInput } from 'react-admin';
|
import { Edit, SimpleForm, TextInput, NumberInput } from 'react-admin';
|
||||||
|
|
||||||
|
|
||||||
export const EquipmentTypeEdit = () => (
|
export const EquipmentTypeEdit = () => (
|
||||||
<Edit>
|
<Edit>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<TextInput source="code" label="Код" disabled />
|
<TextInput source="code" label="Код вида оборудования" disabled />
|
||||||
<TextInput source="name" label="Наименование" isRequired />
|
<TextInput source="name" label="Наименование вида" isRequired />
|
||||||
<TextInput source="manufacturer" label="Производитель" />
|
<TextInput source="manufacturer" label="Производитель" />
|
||||||
<NumberInput source="maintenanceIntervalHours" label="Периодичность ТО (ч)" />
|
<NumberInput source="maintenanceIntervalHours" label="Периодичность ТО, моточасов" />
|
||||||
<NumberInput source="overhaulIntervalHours" label="Периодичность КР (ч)" />
|
<NumberInput source="overhaulIntervalHours" label="Периодичность КР, моточасов" />
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</Edit>
|
</Edit>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,38 @@
|
|||||||
import { List, Datagrid, TextField, NumberField } from 'react-admin';
|
import {
|
||||||
|
List,
|
||||||
|
Datagrid,
|
||||||
|
TextField,
|
||||||
|
TextInput,
|
||||||
|
TopToolbar,
|
||||||
|
FilterButton,
|
||||||
|
CreateButton,
|
||||||
|
ExportButton,
|
||||||
|
NumberField
|
||||||
|
} from 'react-admin';
|
||||||
|
|
||||||
|
|
||||||
|
const equipmentTypeFilters = [
|
||||||
|
<TextInput key="q" source="q" label="Поиск" alwaysOn />,
|
||||||
|
<TextInput key="name" source="name" label="Наименование вида" />,
|
||||||
|
<TextInput key="manufacturer" source="manufacturer" label="Производитель" />
|
||||||
|
];
|
||||||
|
|
||||||
|
const EquipmentTypeListActions = () => (
|
||||||
|
<TopToolbar>
|
||||||
|
<FilterButton filters={equipmentTypeFilters} />
|
||||||
|
<CreateButton />
|
||||||
|
<ExportButton />
|
||||||
|
</TopToolbar>
|
||||||
|
);
|
||||||
|
|
||||||
export const EquipmentTypeList = () => (
|
export const EquipmentTypeList = () => (
|
||||||
<List>
|
<List actions={<EquipmentTypeListActions />} filters={equipmentTypeFilters} sort={{ field: 'code', order: 'ASC' }}>
|
||||||
<Datagrid rowClick="show">
|
<Datagrid rowClick="show">
|
||||||
<TextField source="code" label="Код" />
|
<TextField source="code" label="Код вида оборудования" />
|
||||||
<TextField source="name" label="Наименование" />
|
<TextField source="name" label="Наименование вида" />
|
||||||
<TextField source="manufacturer" label="Производитель" />
|
<TextField source="manufacturer" label="Производитель" />
|
||||||
<NumberField source="maintenanceIntervalHours" label="Периодичность ТО (ч)" />
|
<NumberField source="maintenanceIntervalHours" label="Периодичность ТО, моточасов" />
|
||||||
<NumberField source="overhaulIntervalHours" label="Периодичность КР (ч)" />
|
<NumberField source="overhaulIntervalHours" label="Периодичность КР, моточасов" />
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { Show, SimpleShowLayout, TextField, NumberField } from 'react-admin';
|
|||||||
export const EquipmentTypeShow = () => (
|
export const EquipmentTypeShow = () => (
|
||||||
<Show>
|
<Show>
|
||||||
<SimpleShowLayout>
|
<SimpleShowLayout>
|
||||||
<TextField source="code" label="Код" />
|
<TextField source="code" label="Код вида оборудования" />
|
||||||
<TextField source="name" label="Наименование" />
|
<TextField source="name" label="Наименование вида" />
|
||||||
<TextField source="manufacturer" label="Производитель" />
|
<TextField source="manufacturer" label="Производитель" />
|
||||||
<NumberField source="maintenanceIntervalHours" label="Периодичность ТО (ч)" />
|
<NumberField source="maintenanceIntervalHours" label="Периодичность ТО, моточасов" />
|
||||||
<NumberField source="overhaulIntervalHours" label="Периодичность КР (ч)" />
|
<NumberField source="overhaulIntervalHours" label="Периодичность КР, моточасов" />
|
||||||
</SimpleShowLayout>
|
</SimpleShowLayout>
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { Create, SimpleForm, TextInput, NumberInput, DateInput, SelectInput, ReferenceInput, AutocompleteInput } from 'react-admin';
|
||||||
Create,
|
|
||||||
SimpleForm,
|
|
||||||
TextInput,
|
|
||||||
NumberInput,
|
|
||||||
DateInput,
|
|
||||||
SelectInput,
|
|
||||||
ReferenceInput,
|
|
||||||
} from 'react-admin';
|
|
||||||
|
|
||||||
const statusChoices = [
|
const statusChoices = [
|
||||||
{ id: 'Active', name: 'В эксплуатации' },
|
{ id: 'Active', name: 'В эксплуатации' },
|
||||||
@@ -19,18 +11,18 @@ export const EquipmentCreate = () => (
|
|||||||
<Create>
|
<Create>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<TextInput source="inventoryNumber" label="Инвентарный номер" isRequired />
|
<TextInput source="inventoryNumber" label="Инвентарный номер" isRequired />
|
||||||
<TextInput source="serialNumber" label="Заводской номер" />
|
<TextInput source="serialNumber" label="Заводской (серийный) номер" />
|
||||||
<TextInput source="name" label="Наименование" isRequired />
|
<TextInput source="name" label="Наименование единицы оборудования" isRequired />
|
||||||
<ReferenceInput source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования">
|
<ReferenceInput source="equipmentTypeCode" reference="equipment-types">
|
||||||
<SelectInput optionText="name" optionValue="code" isRequired />
|
<AutocompleteInput label="Вид оборудования" optionText={(record) => record.code ? `${record.code} — ${record.name ?? record.code}` : (record.name ?? record.id)} filterToQuery={(searchText) => ({ q: searchText })} />
|
||||||
</ReferenceInput>
|
</ReferenceInput>
|
||||||
<SelectInput source="status" label="Статус" choices={statusChoices} defaultValue="Active" />
|
<SelectInput source="status" label="Текущий статус" choices={statusChoices} emptyText="Не выбрано" />
|
||||||
<TextInput source="location" label="Место эксплуатации" />
|
<TextInput source="location" label="Место эксплуатации / скважина / куст" />
|
||||||
<DateInput source="commissionedAt" label="Дата ввода в эксплуатацию" />
|
<DateInput source="commissionedAt" label="Дата ввода в эксплуатацию" />
|
||||||
<NumberInput source="totalEngineHours" label="Общая наработка (ч)" />
|
<NumberInput source="totalEngineHours" label="Общая наработка, моточасов" />
|
||||||
<NumberInput source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта (ч)" />
|
<NumberInput source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта, моточасов" />
|
||||||
<DateInput source="lastRepairAt" label="Дата последнего ремонта" />
|
<DateInput source="lastRepairAt" label="Дата последнего ремонта" />
|
||||||
<TextInput source="notes" label="Примечания" multiline />
|
<TextInput source="notes" label="Примечания" />
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</Create>
|
</Create>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { Edit, SimpleForm, TextInput, NumberInput, DateInput, SelectInput, ReferenceInput, AutocompleteInput } from 'react-admin';
|
||||||
Edit,
|
|
||||||
SimpleForm,
|
|
||||||
TextInput,
|
|
||||||
NumberInput,
|
|
||||||
DateInput,
|
|
||||||
SelectInput,
|
|
||||||
ReferenceInput,
|
|
||||||
} from 'react-admin';
|
|
||||||
|
|
||||||
const statusChoices = [
|
const statusChoices = [
|
||||||
{ id: 'Active', name: 'В эксплуатации' },
|
{ id: 'Active', name: 'В эксплуатации' },
|
||||||
@@ -18,19 +10,20 @@ const statusChoices = [
|
|||||||
export const EquipmentEdit = () => (
|
export const EquipmentEdit = () => (
|
||||||
<Edit>
|
<Edit>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
|
<TextInput source="id" label="id" disabled />
|
||||||
<TextInput source="inventoryNumber" label="Инвентарный номер" isRequired />
|
<TextInput source="inventoryNumber" label="Инвентарный номер" isRequired />
|
||||||
<TextInput source="serialNumber" label="Заводской номер" />
|
<TextInput source="serialNumber" label="Заводской (серийный) номер" />
|
||||||
<TextInput source="name" label="Наименование" isRequired />
|
<TextInput source="name" label="Наименование единицы оборудования" isRequired />
|
||||||
<ReferenceInput source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования">
|
<ReferenceInput source="equipmentTypeCode" reference="equipment-types">
|
||||||
<SelectInput optionText="name" optionValue="code" isRequired />
|
<AutocompleteInput label="Вид оборудования" optionText={(record) => record.code ? `${record.code} — ${record.name ?? record.code}` : (record.name ?? record.id)} filterToQuery={(searchText) => ({ q: searchText })} />
|
||||||
</ReferenceInput>
|
</ReferenceInput>
|
||||||
<SelectInput source="status" label="Статус" choices={statusChoices} />
|
<SelectInput source="status" label="Текущий статус" choices={statusChoices} emptyText="Не выбрано" />
|
||||||
<TextInput source="location" label="Место эксплуатации" />
|
<TextInput source="location" label="Место эксплуатации / скважина / куст" />
|
||||||
<DateInput source="commissionedAt" label="Дата ввода в эксплуатацию" />
|
<DateInput source="commissionedAt" label="Дата ввода в эксплуатацию" />
|
||||||
<NumberInput source="totalEngineHours" label="Общая наработка (ч)" />
|
<NumberInput source="totalEngineHours" label="Общая наработка, моточасов" />
|
||||||
<NumberInput source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта (ч)" />
|
<NumberInput source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта, моточасов" />
|
||||||
<DateInput source="lastRepairAt" label="Дата последнего ремонта" />
|
<DateInput source="lastRepairAt" label="Дата последнего ремонта" />
|
||||||
<TextInput source="notes" label="Примечания" multiline />
|
<TextInput source="notes" label="Примечания" />
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</Edit>
|
</Edit>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,9 +2,18 @@ import {
|
|||||||
List,
|
List,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
TextField,
|
TextField,
|
||||||
|
TextInput,
|
||||||
|
TopToolbar,
|
||||||
|
FilterButton,
|
||||||
|
CreateButton,
|
||||||
|
ExportButton,
|
||||||
NumberField,
|
NumberField,
|
||||||
|
DateField,
|
||||||
SelectField,
|
SelectField,
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
|
SelectArrayInput,
|
||||||
|
ReferenceInput,
|
||||||
|
AutocompleteInput
|
||||||
} from 'react-admin';
|
} from 'react-admin';
|
||||||
|
|
||||||
const statusChoices = [
|
const statusChoices = [
|
||||||
@@ -14,17 +23,44 @@ const statusChoices = [
|
|||||||
{ id: 'WriteOff', name: 'Списано' },
|
{ id: 'WriteOff', name: 'Списано' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const equipmentFilters = [
|
||||||
|
<TextInput key="q" source="q" label="Поиск" alwaysOn />,
|
||||||
|
<TextInput key="inventoryNumber" source="inventoryNumber" label="Инвентарный номер" />,
|
||||||
|
<TextInput key="serialNumber" source="serialNumber" label="Заводской (серийный) номер" />,
|
||||||
|
<TextInput key="name" source="name" label="Наименование единицы оборудования" />,
|
||||||
|
<ReferenceInput key="equipmentTypeCode" source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования">
|
||||||
|
<AutocompleteInput optionText={(record) => record.code ? `${record.code} — ${record.name ?? record.code}` : (record.name ?? record.id)} filterToQuery={(searchText) => ({ q: searchText })} />
|
||||||
|
</ReferenceInput>,
|
||||||
|
<SelectArrayInput key="status" source="status" label="Текущий статус" choices={statusChoices} />,
|
||||||
|
<TextInput key="location" source="location" label="Место эксплуатации / скважина / куст" />,
|
||||||
|
<TextInput key="notes" source="notes" label="Примечания" />
|
||||||
|
];
|
||||||
|
|
||||||
|
const EquipmentListActions = () => (
|
||||||
|
<TopToolbar>
|
||||||
|
<FilterButton filters={equipmentFilters} />
|
||||||
|
<CreateButton />
|
||||||
|
<ExportButton />
|
||||||
|
</TopToolbar>
|
||||||
|
);
|
||||||
|
|
||||||
export const EquipmentList = () => (
|
export const EquipmentList = () => (
|
||||||
<List>
|
<List actions={<EquipmentListActions />} filters={equipmentFilters} sort={{ field: 'inventoryNumber', order: 'ASC' }}>
|
||||||
<Datagrid rowClick="show">
|
<Datagrid rowClick="show">
|
||||||
|
<TextField source="id" label="id" />
|
||||||
<TextField source="inventoryNumber" label="Инвентарный номер" />
|
<TextField source="inventoryNumber" label="Инвентарный номер" />
|
||||||
<TextField source="name" label="Наименование" />
|
<TextField source="serialNumber" label="Заводской (серийный) номер" />
|
||||||
|
<TextField source="name" label="Наименование единицы оборудования" />
|
||||||
<ReferenceField source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования" link="show">
|
<ReferenceField source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования" link="show">
|
||||||
<TextField source="name" />
|
<TextField source="code" />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
<SelectField source="status" label="Статус" choices={statusChoices} />
|
<SelectField source="status" label="Текущий статус" choices={statusChoices} />
|
||||||
<TextField source="location" label="Место эксплуатации" />
|
<TextField source="location" label="Место эксплуатации / скважина / куст" />
|
||||||
<NumberField source="totalEngineHours" label="Наработка (ч)" />
|
<DateField source="commissionedAt" label="Дата ввода в эксплуатацию" />
|
||||||
|
<NumberField source="totalEngineHours" label="Общая наработка, моточасов" />
|
||||||
|
<NumberField source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта, моточасов" />
|
||||||
|
<DateField source="lastRepairAt" label="Дата последнего ремонта" />
|
||||||
|
<TextField source="notes" label="Примечания" />
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { Show, SimpleShowLayout, TextField, NumberField, DateField, SelectField, ReferenceField } from 'react-admin';
|
||||||
Show,
|
|
||||||
SimpleShowLayout,
|
|
||||||
TextField,
|
|
||||||
NumberField,
|
|
||||||
DateField,
|
|
||||||
SelectField,
|
|
||||||
ReferenceField,
|
|
||||||
} from 'react-admin';
|
|
||||||
|
|
||||||
const statusChoices = [
|
const statusChoices = [
|
||||||
{ id: 'Active', name: 'В эксплуатации' },
|
{ id: 'Active', name: 'В эксплуатации' },
|
||||||
@@ -14,21 +6,21 @@ const statusChoices = [
|
|||||||
{ id: 'Reserve', name: 'В резерве' },
|
{ id: 'Reserve', name: 'В резерве' },
|
||||||
{ id: 'WriteOff', name: 'Списано' },
|
{ id: 'WriteOff', name: 'Списано' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const EquipmentShow = () => (
|
export const EquipmentShow = () => (
|
||||||
<Show>
|
<Show>
|
||||||
<SimpleShowLayout>
|
<SimpleShowLayout>
|
||||||
|
<TextField source="id" label="id" />
|
||||||
<TextField source="inventoryNumber" label="Инвентарный номер" />
|
<TextField source="inventoryNumber" label="Инвентарный номер" />
|
||||||
<TextField source="serialNumber" label="Заводской номер" />
|
<TextField source="serialNumber" label="Заводской (серийный) номер" />
|
||||||
<TextField source="name" label="Наименование" />
|
<TextField source="name" label="Наименование единицы оборудования" />
|
||||||
<ReferenceField source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования" link="show">
|
<ReferenceField source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования" link="show">
|
||||||
<TextField source="name" />
|
<TextField source="code" />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
<SelectField source="status" label="Статус" choices={statusChoices} />
|
<SelectField source="status" label="Текущий статус" choices={statusChoices} />
|
||||||
<TextField source="location" label="Место эксплуатации" />
|
<TextField source="location" label="Место эксплуатации / скважина / куст" />
|
||||||
<DateField source="commissionedAt" label="Дата ввода в эксплуатацию" />
|
<DateField source="commissionedAt" label="Дата ввода в эксплуатацию" />
|
||||||
<NumberField source="totalEngineHours" label="Общая наработка (ч)" />
|
<NumberField source="totalEngineHours" label="Общая наработка, моточасов" />
|
||||||
<NumberField source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта (ч)" />
|
<NumberField source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта, моточасов" />
|
||||||
<DateField source="lastRepairAt" label="Дата последнего ремонта" />
|
<DateField source="lastRepairAt" label="Дата последнего ремонта" />
|
||||||
<TextField source="notes" label="Примечания" />
|
<TextField source="notes" label="Примечания" />
|
||||||
</SimpleShowLayout>
|
</SimpleShowLayout>
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { Create, SimpleForm, TextInput, NumberInput, DateInput, SelectInput, ReferenceInput, AutocompleteInput } from 'react-admin';
|
||||||
Create,
|
|
||||||
SimpleForm,
|
|
||||||
TextInput,
|
|
||||||
NumberInput,
|
|
||||||
DateInput,
|
|
||||||
SelectInput,
|
|
||||||
ReferenceInput,
|
|
||||||
} from 'react-admin';
|
|
||||||
|
|
||||||
const repairKindChoices = [
|
const repairKindChoices = [
|
||||||
{ id: 'TO', name: 'Техническое обслуживание' },
|
{ id: 'TO', name: 'Техническое обслуживание' },
|
||||||
@@ -29,18 +21,18 @@ export const RepairOrderCreate = () => (
|
|||||||
<Create>
|
<Create>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<TextInput source="number" label="Номер заявки" isRequired />
|
<TextInput source="number" label="Номер заявки" isRequired />
|
||||||
<ReferenceInput source="equipmentId" reference="equipment" label="Оборудование">
|
<ReferenceInput source="equipmentId" reference="equipment">
|
||||||
<SelectInput optionText="name" isRequired />
|
<AutocompleteInput label="Оборудование" optionText={(record) => record.inventoryNumber ? `${record.inventoryNumber} — ${record.name ?? record.inventoryNumber}` : (record.name ?? record.id)} filterToQuery={(searchText) => ({ q: searchText })} />
|
||||||
</ReferenceInput>
|
</ReferenceInput>
|
||||||
<SelectInput source="repairKind" label="Вид ремонта" choices={repairKindChoices} isRequired />
|
<SelectInput source="repairKind" label="Вид ремонта" choices={repairKindChoices} emptyText="Не выбрано" />
|
||||||
<SelectInput source="status" label="Статус" choices={statusChoices} defaultValue="Draft" />
|
<SelectInput source="status" label="Статус" choices={statusChoices} emptyText="Не выбрано" />
|
||||||
<DateInput source="plannedAt" label="Плановая дата начала" isRequired />
|
<DateInput source="plannedAt" label="Плановая дата начала" />
|
||||||
<DateInput source="startedAt" label="Фактическая дата начала" />
|
<DateInput source="startedAt" label="Фактическая дата начала" />
|
||||||
<DateInput source="completedAt" label="Фактическая дата завершения" />
|
<DateInput source="completedAt" label="Фактическая дата завершения" />
|
||||||
<TextInput source="contractor" label="Подрядная организация" />
|
<TextInput source="contractor" label="Подрядная организация (если внешний ремонт)" />
|
||||||
<NumberInput source="engineHoursAtRepair" label="Наработка на момент ремонта (ч)" />
|
<NumberInput source="engineHoursAtRepair" label="Наработка на момент ремонта, моточасов" />
|
||||||
<TextInput source="description" label="Описание работ / дефекта" multiline />
|
<TextInput source="description" label="Описание работ / дефекта" />
|
||||||
<TextInput source="notes" label="Примечания" multiline />
|
<TextInput source="notes" label="Примечания" />
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</Create>
|
</Create>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { Edit, SimpleForm, TextInput, NumberInput, DateInput, SelectInput, ReferenceInput, AutocompleteInput } from 'react-admin';
|
||||||
Edit,
|
|
||||||
SimpleForm,
|
|
||||||
TextInput,
|
|
||||||
NumberInput,
|
|
||||||
DateInput,
|
|
||||||
SelectInput,
|
|
||||||
ReferenceInput,
|
|
||||||
} from 'react-admin';
|
|
||||||
|
|
||||||
const repairKindChoices = [
|
const repairKindChoices = [
|
||||||
{ id: 'TO', name: 'Техническое обслуживание' },
|
{ id: 'TO', name: 'Техническое обслуживание' },
|
||||||
@@ -28,19 +20,20 @@ const statusChoices = [
|
|||||||
export const RepairOrderEdit = () => (
|
export const RepairOrderEdit = () => (
|
||||||
<Edit>
|
<Edit>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
|
<TextInput source="id" label="id" disabled />
|
||||||
<TextInput source="number" label="Номер заявки" isRequired />
|
<TextInput source="number" label="Номер заявки" isRequired />
|
||||||
<ReferenceInput source="equipmentId" reference="equipment" label="Оборудование">
|
<ReferenceInput source="equipmentId" reference="equipment">
|
||||||
<SelectInput optionText="name" isRequired />
|
<AutocompleteInput label="Оборудование" optionText={(record) => record.inventoryNumber ? `${record.inventoryNumber} — ${record.name ?? record.inventoryNumber}` : (record.name ?? record.id)} filterToQuery={(searchText) => ({ q: searchText })} />
|
||||||
</ReferenceInput>
|
</ReferenceInput>
|
||||||
<SelectInput source="repairKind" label="Вид ремонта" choices={repairKindChoices} isRequired />
|
<SelectInput source="repairKind" label="Вид ремонта" choices={repairKindChoices} emptyText="Не выбрано" />
|
||||||
<SelectInput source="status" label="Статус" choices={statusChoices} />
|
<SelectInput source="status" label="Статус" choices={statusChoices} emptyText="Не выбрано" />
|
||||||
<DateInput source="plannedAt" label="Плановая дата начала" isRequired />
|
<DateInput source="plannedAt" label="Плановая дата начала" />
|
||||||
<DateInput source="startedAt" label="Фактическая дата начала" />
|
<DateInput source="startedAt" label="Фактическая дата начала" />
|
||||||
<DateInput source="completedAt" label="Фактическая дата завершения" />
|
<DateInput source="completedAt" label="Фактическая дата завершения" />
|
||||||
<TextInput source="contractor" label="Подрядная организация" />
|
<TextInput source="contractor" label="Подрядная организация (если внешний ремонт)" />
|
||||||
<NumberInput source="engineHoursAtRepair" label="Наработка на момент ремонта (ч)" />
|
<NumberInput source="engineHoursAtRepair" label="Наработка на момент ремонта, моточасов" />
|
||||||
<TextInput source="description" label="Описание работ / дефекта" multiline />
|
<TextInput source="description" label="Описание работ / дефекта" />
|
||||||
<TextInput source="notes" label="Примечания" multiline />
|
<TextInput source="notes" label="Примечания" />
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</Edit>
|
</Edit>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,9 +2,19 @@ import {
|
|||||||
List,
|
List,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
TextField,
|
TextField,
|
||||||
|
TextInput,
|
||||||
|
TopToolbar,
|
||||||
|
FilterButton,
|
||||||
|
CreateButton,
|
||||||
|
ExportButton,
|
||||||
|
NumberField,
|
||||||
DateField,
|
DateField,
|
||||||
SelectField,
|
SelectField,
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
|
SelectArrayInput,
|
||||||
|
SelectInput,
|
||||||
|
ReferenceInput,
|
||||||
|
AutocompleteInput
|
||||||
} from 'react-admin';
|
} from 'react-admin';
|
||||||
|
|
||||||
const repairKindChoices = [
|
const repairKindChoices = [
|
||||||
@@ -24,17 +34,44 @@ const statusChoices = [
|
|||||||
{ id: 'Cancelled', name: 'Отменена' },
|
{ id: 'Cancelled', name: 'Отменена' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const repairOrderFilters = [
|
||||||
|
<TextInput key="q" source="q" label="Поиск" alwaysOn />,
|
||||||
|
<TextInput key="number" source="number" label="Номер заявки" />,
|
||||||
|
<ReferenceInput key="equipmentId" source="equipmentId" reference="equipment" label="Оборудование">
|
||||||
|
<AutocompleteInput optionText={(record) => record.inventoryNumber ? `${record.inventoryNumber} — ${record.name ?? record.inventoryNumber}` : (record.name ?? record.id)} filterToQuery={(searchText) => ({ q: searchText })} />
|
||||||
|
</ReferenceInput>,
|
||||||
|
<SelectInput key="repairKind" source="repairKind" label="Вид ремонта" choices={repairKindChoices} emptyText="Все" />,
|
||||||
|
<SelectArrayInput key="status" source="status" label="Статус" choices={statusChoices} />,
|
||||||
|
<TextInput key="contractor" source="contractor" label="Подрядная организация (если внешний ремонт)" />,
|
||||||
|
<TextInput key="description" source="description" label="Описание работ / дефекта" />,
|
||||||
|
<TextInput key="notes" source="notes" label="Примечания" />
|
||||||
|
];
|
||||||
|
|
||||||
|
const RepairOrderListActions = () => (
|
||||||
|
<TopToolbar>
|
||||||
|
<FilterButton filters={repairOrderFilters} />
|
||||||
|
<CreateButton />
|
||||||
|
<ExportButton />
|
||||||
|
</TopToolbar>
|
||||||
|
);
|
||||||
|
|
||||||
export const RepairOrderList = () => (
|
export const RepairOrderList = () => (
|
||||||
<List>
|
<List actions={<RepairOrderListActions />} filters={repairOrderFilters} sort={{ field: 'number', order: 'ASC' }}>
|
||||||
<Datagrid rowClick="show">
|
<Datagrid rowClick="show">
|
||||||
<TextField source="number" label="Номер" />
|
<TextField source="id" label="id" />
|
||||||
|
<TextField source="number" label="Номер заявки" />
|
||||||
<ReferenceField source="equipmentId" reference="equipment" label="Оборудование" link="show">
|
<ReferenceField source="equipmentId" reference="equipment" label="Оборудование" link="show">
|
||||||
<TextField source="name" />
|
<TextField source="inventoryNumber" />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
<SelectField source="repairKind" label="Вид ремонта" choices={repairKindChoices} />
|
<SelectField source="repairKind" label="Вид ремонта" choices={repairKindChoices} />
|
||||||
<SelectField source="status" label="Статус" choices={statusChoices} />
|
<SelectField source="status" label="Статус" choices={statusChoices} />
|
||||||
<DateField source="plannedAt" label="Плановая дата" />
|
<DateField source="plannedAt" label="Плановая дата начала" />
|
||||||
<TextField source="contractor" label="Подрядчик" />
|
<DateField source="startedAt" label="Фактическая дата начала" />
|
||||||
|
<DateField source="completedAt" label="Фактическая дата завершения" />
|
||||||
|
<TextField source="contractor" label="Подрядная организация (если внешний ремонт)" />
|
||||||
|
<NumberField source="engineHoursAtRepair" label="Наработка на момент ремонта, моточасов" />
|
||||||
|
<TextField source="description" label="Описание работ / дефекта" />
|
||||||
|
<TextField source="notes" label="Примечания" />
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { Show, SimpleShowLayout, TextField, NumberField, DateField, SelectField, ReferenceField } from 'react-admin';
|
||||||
Show,
|
|
||||||
SimpleShowLayout,
|
|
||||||
TextField,
|
|
||||||
NumberField,
|
|
||||||
DateField,
|
|
||||||
SelectField,
|
|
||||||
ReferenceField,
|
|
||||||
} from 'react-admin';
|
|
||||||
|
|
||||||
const repairKindChoices = [
|
const repairKindChoices = [
|
||||||
{ id: 'TO', name: 'Техническое обслуживание' },
|
{ id: 'TO', name: 'Техническое обслуживание' },
|
||||||
@@ -24,21 +16,21 @@ const statusChoices = [
|
|||||||
{ id: 'Done', name: 'Выполнена' },
|
{ id: 'Done', name: 'Выполнена' },
|
||||||
{ id: 'Cancelled', name: 'Отменена' },
|
{ id: 'Cancelled', name: 'Отменена' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const RepairOrderShow = () => (
|
export const RepairOrderShow = () => (
|
||||||
<Show>
|
<Show>
|
||||||
<SimpleShowLayout>
|
<SimpleShowLayout>
|
||||||
|
<TextField source="id" label="id" />
|
||||||
<TextField source="number" label="Номер заявки" />
|
<TextField source="number" label="Номер заявки" />
|
||||||
<ReferenceField source="equipmentId" reference="equipment" label="Оборудование" link="show">
|
<ReferenceField source="equipmentId" reference="equipment" label="Оборудование" link="show">
|
||||||
<TextField source="name" />
|
<TextField source="inventoryNumber" />
|
||||||
</ReferenceField>
|
</ReferenceField>
|
||||||
<SelectField source="repairKind" label="Вид ремонта" choices={repairKindChoices} />
|
<SelectField source="repairKind" label="Вид ремонта" choices={repairKindChoices} />
|
||||||
<SelectField source="status" label="Статус" choices={statusChoices} />
|
<SelectField source="status" label="Статус" choices={statusChoices} />
|
||||||
<DateField source="plannedAt" label="Плановая дата начала" />
|
<DateField source="plannedAt" label="Плановая дата начала" />
|
||||||
<DateField source="startedAt" label="Фактическая дата начала" />
|
<DateField source="startedAt" label="Фактическая дата начала" />
|
||||||
<DateField source="completedAt" label="Фактическая дата завершения" />
|
<DateField source="completedAt" label="Фактическая дата завершения" />
|
||||||
<TextField source="contractor" label="Подрядная организация" />
|
<TextField source="contractor" label="Подрядная организация (если внешний ремонт)" />
|
||||||
<NumberField source="engineHoursAtRepair" label="Наработка на момент ремонта (ч)" />
|
<NumberField source="engineHoursAtRepair" label="Наработка на момент ремонта, моточасов" />
|
||||||
<TextField source="description" label="Описание работ / дефекта" />
|
<TextField source="description" label="Описание работ / дефекта" />
|
||||||
<TextField source="notes" label="Примечания" />
|
<TextField source="notes" label="Примечания" />
|
||||||
</SimpleShowLayout>
|
</SimpleShowLayout>
|
||||||
|
|||||||
@@ -233,3 +233,4 @@ entity RepairOrder {
|
|||||||
type text;
|
type text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
713
generation/generate.mjs
Normal file
713
generation/generate.mjs
Normal file
@@ -0,0 +1,713 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
// Always resolve repo root relative to this script location
|
||||||
|
// <repo>/generation/generate.mjs -> root is parent folder of generation/
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
function readFile(p) {
|
||||||
|
return fs.readFileSync(p, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFile(p, content) {
|
||||||
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||||
|
fs.writeFileSync(p, content, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toKebab(s) {
|
||||||
|
return s
|
||||||
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
||||||
|
.replace(/_/g, '-')
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pluralize(resource) {
|
||||||
|
// Minimal heuristic; can be improved later.
|
||||||
|
if (resource === 'equipment') return 'equipment';
|
||||||
|
if (resource.endsWith('s')) return `${resource}es`;
|
||||||
|
return `${resource}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function upperFirst(s) {
|
||||||
|
return s ? s[0].toUpperCase() + s.slice(1) : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lowerFirst(s) {
|
||||||
|
return s ? s[0].toLowerCase() + s.slice(1) : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIdentifierFromKebab(kebab) {
|
||||||
|
// "repair-order" -> "repairOrder"
|
||||||
|
return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBlocks(text, kind) {
|
||||||
|
// kind: 'enum' | 'entity'
|
||||||
|
const blocks = [];
|
||||||
|
const lines = text.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const m = line.match(new RegExp(`^\\s*${kind}\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\{\\s*$`));
|
||||||
|
if (!m) continue;
|
||||||
|
const name = m[1];
|
||||||
|
let depth = 0;
|
||||||
|
const start = i;
|
||||||
|
let end = i;
|
||||||
|
for (let j = i; j < lines.length; j++) {
|
||||||
|
const l = lines[j];
|
||||||
|
if (l.includes('{')) depth += (l.match(/\{/g) || []).length;
|
||||||
|
if (l.includes('}')) depth -= (l.match(/\}/g) || []).length;
|
||||||
|
if (depth === 0 && j > i) {
|
||||||
|
end = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const body = lines.slice(start + 1, end).join('\n');
|
||||||
|
blocks.push({ name, body, startLine: start, endLine: end });
|
||||||
|
i = end;
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEnum(body) {
|
||||||
|
const values = [];
|
||||||
|
const labels = {};
|
||||||
|
const re = /value\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{([\s\S]*?)\}/gm;
|
||||||
|
let m;
|
||||||
|
while ((m = re.exec(body))) {
|
||||||
|
values.push(m[1]);
|
||||||
|
const label = (m[2].match(/label\s+"([^"]+)"/m) || [])[1];
|
||||||
|
labels[m[1]] = label || m[1];
|
||||||
|
}
|
||||||
|
return { values, labels };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEntity(body) {
|
||||||
|
const attrs = [];
|
||||||
|
const entityLabel = (body.match(/description\s+"([^"]+)"/m) || [])[1];
|
||||||
|
const lines = body.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const m = lines[i].match(/^\s*attribute\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{\s*$/);
|
||||||
|
if (!m) continue;
|
||||||
|
const name = m[1];
|
||||||
|
let depth = 0;
|
||||||
|
const start = i;
|
||||||
|
let end = i;
|
||||||
|
for (let j = i; j < lines.length; j++) {
|
||||||
|
const l = lines[j];
|
||||||
|
if (l.includes('{')) depth += (l.match(/\{/g) || []).length;
|
||||||
|
if (l.includes('}')) depth -= (l.match(/\}/g) || []).length;
|
||||||
|
if (depth === 0 && j > i) {
|
||||||
|
end = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const abody = lines.slice(start + 1, end).join('\n');
|
||||||
|
const type = (abody.match(/^\s*type\s+([A-Za-z_][A-Za-z0-9_]*)\s*;/m) || [])[1];
|
||||||
|
const isRequired = /^\s*is required\s*;/m.test(abody);
|
||||||
|
const isUnique = /^\s*is unique\s*;/m.test(abody);
|
||||||
|
const isPrimary = /^\s*key primary\s*;/m.test(abody);
|
||||||
|
const defaultValue = (abody.match(/^\s*default\s+([A-Za-z_][A-Za-z0-9_]*)\s*;/m) || [])[1];
|
||||||
|
const foreignRel = (abody.match(/relates\s+([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\s*;/m) || []).slice(1);
|
||||||
|
const description = (abody.match(/description\s+"([^"]+)"/m) || [])[1];
|
||||||
|
|
||||||
|
attrs.push({
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
label: description || name,
|
||||||
|
isRequired,
|
||||||
|
isUnique,
|
||||||
|
isPrimary,
|
||||||
|
defaultValue,
|
||||||
|
foreign: foreignRel.length ? { entity: foreignRel[0], field: foreignRel[1] } : null,
|
||||||
|
});
|
||||||
|
i = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pk = attrs.find((a) => a.isPrimary);
|
||||||
|
if (!pk) throw new Error('Entity missing primary key attribute');
|
||||||
|
|
||||||
|
return { attributes: attrs, primaryKey: pk.name, label: entityLabel };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDomainDSL(dslText) {
|
||||||
|
const enums = {};
|
||||||
|
const entities = {};
|
||||||
|
|
||||||
|
for (const b of parseBlocks(dslText, 'enum')) {
|
||||||
|
enums[b.name] = parseEnum(b.body);
|
||||||
|
}
|
||||||
|
for (const b of parseBlocks(dslText, 'entity')) {
|
||||||
|
entities[b.name] = parseEntity(b.body);
|
||||||
|
}
|
||||||
|
return { enums, entities };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntityAttrNames(entity) {
|
||||||
|
return new Set(entity.attributes.map((a) => a.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBestSortField(entity, pk) {
|
||||||
|
const attrs = getEntityAttrNames(entity);
|
||||||
|
if (attrs.has('inventoryNumber')) return 'inventoryNumber';
|
||||||
|
if (attrs.has('number')) return 'number';
|
||||||
|
if (attrs.has('code')) return 'code';
|
||||||
|
if (attrs.has('name')) return 'name';
|
||||||
|
return pk;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReferenceDisplayExpr(foreignEntity) {
|
||||||
|
const attrs = getEntityAttrNames(foreignEntity);
|
||||||
|
if (attrs.has('inventoryNumber')) {
|
||||||
|
return "(record) => record.inventoryNumber ? `${record.inventoryNumber} — ${record.name ?? record.inventoryNumber}` : (record.name ?? record.id)";
|
||||||
|
}
|
||||||
|
if (attrs.has('code')) {
|
||||||
|
return "(record) => record.code ? `${record.code} — ${record.name ?? record.code}` : (record.name ?? record.id)";
|
||||||
|
}
|
||||||
|
if (attrs.has('number')) {
|
||||||
|
return "(record) => record.number ? `${record.number} — ${record.name ?? record.number}` : (record.name ?? record.id)";
|
||||||
|
}
|
||||||
|
if (attrs.has('name')) {
|
||||||
|
return "(record) => record.name ?? record.id";
|
||||||
|
}
|
||||||
|
return "(record) => record.id";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAttributeLabel(attr, allEntities) {
|
||||||
|
if (attr.label && attr.label !== attr.name) return attr.label;
|
||||||
|
if (attr.name === 'status') return 'Статус';
|
||||||
|
if (attr.name === 'equipmentId') return 'Оборудование';
|
||||||
|
if (attr.name === 'equipmentTypeCode') return 'Вид оборудования';
|
||||||
|
if (attr.foreign) return allEntities[attr.foreign.entity]?.label || attr.name;
|
||||||
|
return attr.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prismaScalarType(dslType) {
|
||||||
|
switch (dslType) {
|
||||||
|
case 'string':
|
||||||
|
case 'text':
|
||||||
|
return 'String';
|
||||||
|
case 'uuid':
|
||||||
|
return 'String';
|
||||||
|
case 'integer':
|
||||||
|
return 'Int';
|
||||||
|
case 'decimal':
|
||||||
|
return 'Decimal';
|
||||||
|
case 'date':
|
||||||
|
return 'DateTime';
|
||||||
|
default:
|
||||||
|
// enum type name
|
||||||
|
return dslType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePrismaEnum(name, values) {
|
||||||
|
return `enum ${name} {\n${values.map((v) => ` ${v}`).join('\n')}\n}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePrismaModel(name, entity, allEntities) {
|
||||||
|
const lines = [];
|
||||||
|
lines.push(`model ${name} {`);
|
||||||
|
for (const attr of entity.attributes) {
|
||||||
|
if (attr.foreign) {
|
||||||
|
// Keep scalar FK field, relation field added below
|
||||||
|
}
|
||||||
|
const scalar = prismaScalarType(attr.type);
|
||||||
|
const optional = attr.isRequired || attr.isPrimary ? '' : '?';
|
||||||
|
const parts = [` ${attr.name} ${scalar}${optional}`];
|
||||||
|
|
||||||
|
if (attr.isPrimary) {
|
||||||
|
if (attr.type === 'uuid' || attr.name === 'id') parts.push('@id @default(uuid())');
|
||||||
|
else parts.push('@id');
|
||||||
|
}
|
||||||
|
if (attr.isUnique && !attr.isPrimary) parts.push('@unique');
|
||||||
|
if (attr.defaultValue) parts.push(`@default(${attr.defaultValue})`);
|
||||||
|
|
||||||
|
lines.push(`${parts.join(' ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add relations for foreign keys
|
||||||
|
for (const attr of entity.attributes.filter((a) => a.foreign)) {
|
||||||
|
const relEntity = attr.foreign.entity;
|
||||||
|
const relField = attr.foreign.field;
|
||||||
|
const relName = lowerFirst(relEntity);
|
||||||
|
// relation field must not collide; fallback to relEntity name if needed
|
||||||
|
const relationFieldName = entity.attributes.some((a) => a.name === relName) ? `${relName}Ref` : relName;
|
||||||
|
lines.push(
|
||||||
|
` ${relationFieldName} ${relEntity} @relation(fields: [${attr.name}], references: [${relField}])`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add back-relations (required by Prisma when a relation field exists)
|
||||||
|
// For each other entity that has a FK pointing to this model, create a list field.
|
||||||
|
for (const [otherName, otherEntity] of Object.entries(allEntities)) {
|
||||||
|
for (const fk of otherEntity.attributes.filter((a) => a.foreign)) {
|
||||||
|
if (fk.foreign.entity !== name) continue;
|
||||||
|
const candidate = pluralize(lowerFirst(otherName));
|
||||||
|
const fieldName = lines.some((l) => l.startsWith(` ${candidate} `))
|
||||||
|
? `${candidate}List`
|
||||||
|
: candidate;
|
||||||
|
// Avoid duplicates if multiple FKs exist (basic de-dupe)
|
||||||
|
if (lines.some((l) => l.startsWith(` ${fieldName} `))) continue;
|
||||||
|
lines.push(` ${fieldName} ${otherName}[]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('}');
|
||||||
|
return `${lines.join('\n')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePrismaSchema({ enums, entities }, prismaPath, apply) {
|
||||||
|
const existing = fs.existsSync(prismaPath) ? readFile(prismaPath) : '';
|
||||||
|
const hasGenerator = /generator\s+client\s*\{/m.test(existing);
|
||||||
|
const header = hasGenerator
|
||||||
|
? existing.split(/\n(?=enum|model)\b/)[0].trimEnd() + '\n\n'
|
||||||
|
: `generator client {\n provider = "prisma-client-js"\n}\n\ndatasource db {\n provider = "postgresql"\n url = env("DATABASE_URL")\n}\n\n`;
|
||||||
|
|
||||||
|
const out = [header];
|
||||||
|
|
||||||
|
// Preserve existing enum/model blocks not in DSL? For now, regenerate from DSL only.
|
||||||
|
for (const [name, e] of Object.entries(enums)) out.push(generatePrismaEnum(name, e.values) + '\n');
|
||||||
|
for (const [name, ent] of Object.entries(entities)) out.push(generatePrismaModel(name, ent, entities) + '\n');
|
||||||
|
|
||||||
|
const next = out.join('').trimEnd() + '\n';
|
||||||
|
if (!apply) return { changed: next !== existing, content: next };
|
||||||
|
writeFile(prismaPath, next);
|
||||||
|
return { changed: true, content: next };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBackendModule(entityName, entity, resourceName, pk) {
|
||||||
|
const className = entityName;
|
||||||
|
const moduleName = `${className}Module`;
|
||||||
|
const serviceName = `${className}Service`;
|
||||||
|
const controllerName = `${className}Controller`;
|
||||||
|
const folder = toKebab(entityName);
|
||||||
|
|
||||||
|
// DTOs
|
||||||
|
const enumTypes = entity.attributes
|
||||||
|
.filter((a) => !['string', 'text', 'uuid', 'integer', 'decimal', 'date'].includes(a.type))
|
||||||
|
.map((a) => a.type);
|
||||||
|
|
||||||
|
const enumUnion = (typeName) => {
|
||||||
|
// Unknown labels in DSL aren't needed; keep as string union to avoid importing Prisma enums.
|
||||||
|
return `'${typeName}'`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dtoType = (attr) => {
|
||||||
|
switch (attr.type) {
|
||||||
|
case 'uuid':
|
||||||
|
case 'string':
|
||||||
|
case 'text':
|
||||||
|
return 'string';
|
||||||
|
case 'integer':
|
||||||
|
return 'number';
|
||||||
|
case 'decimal':
|
||||||
|
return 'string';
|
||||||
|
case 'date':
|
||||||
|
return 'string';
|
||||||
|
default:
|
||||||
|
// enum
|
||||||
|
return 'string';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDtoLines = [];
|
||||||
|
createDtoLines.push(`export class Create${className}Dto {`);
|
||||||
|
for (const a of entity.attributes) {
|
||||||
|
if (a.isPrimary && a.type === 'uuid') continue; // generated
|
||||||
|
const opt = a.isRequired && !(a.isPrimary && a.type !== 'uuid') ? '!' : '?';
|
||||||
|
createDtoLines.push(` ${a.name}${opt}: ${dtoType(a)};`);
|
||||||
|
}
|
||||||
|
createDtoLines.push('}');
|
||||||
|
|
||||||
|
const updateDtoLines = [];
|
||||||
|
updateDtoLines.push(`export class Update${className}Dto {`);
|
||||||
|
if (pk !== 'id') updateDtoLines.push(` id?: string;`);
|
||||||
|
for (const a of entity.attributes) {
|
||||||
|
if (pk !== 'id' && a.name === 'id') continue;
|
||||||
|
updateDtoLines.push(` ${a.name}?: ${dtoType(a)};`);
|
||||||
|
}
|
||||||
|
updateDtoLines.push('}');
|
||||||
|
|
||||||
|
const controller = `import { Controller, Get, Post, Patch, Delete, Param, Body, Query, Res } from '@nestjs/common';\nimport { Response } from 'express';\nimport { Roles } from '../../auth/decorators/roles.decorator';\nimport { RealmRole } from '../../auth/roles/realm-role.enum';\nimport { ${serviceName} } from './${folder}.service';\nimport { Create${className}Dto } from './dto/create-${folder}.dto';\nimport { Update${className}Dto } from './dto/update-${folder}.dto';\n\n@Controller('${resourceName}')\nexport class ${controllerName} {\n constructor(private readonly service: ${serviceName}) {}\n\n @Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)\n @Get()\n async findAll(@Query() query: any, @Res() res: Response) {\n const result = await this.service.findAll(query);\n res.set('Content-Range', \`${resourceName} \${query._start || 0}-\${query._end || result.total}/\${result.total}\`);\n res.set('Access-Control-Expose-Headers', 'Content-Range');\n return res.json(result.data);\n }\n\n @Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)\n @Get(':${pk}')\n findOne(@Param('${pk}') id: string) {\n return this.service.findOne(id);\n }\n\n @Roles(RealmRole.Editor, RealmRole.Admin)\n @Post()\n create(@Body() dto: Create${className}Dto) {\n return this.service.create(dto);\n }\n\n @Roles(RealmRole.Editor, RealmRole.Admin)\n @Patch(':${pk}')\n update(@Param('${pk}') id: string, @Body() dto: Update${className}Dto) {\n return this.service.update(id, dto);\n }\n\n @Roles(RealmRole.Admin)\n @Delete(':${pk}')\n remove(@Param('${pk}') id: string) {\n return this.service.remove(id);\n }\n}\n`;
|
||||||
|
|
||||||
|
const service = `import { Injectable } from '@nestjs/common';\nimport { Prisma } from '@prisma/client';\nimport { PrismaService } from '../../prisma/prisma.service';\nimport { Create${className}Dto } from './dto/create-${folder}.dto';\nimport { Update${className}Dto } from './dto/update-${folder}.dto';\n\nfunction serializeRecord(record: any) {\n return {\n ...record,\n${entity.attributes
|
||||||
|
.filter((a) => a.type === 'decimal')
|
||||||
|
.map((a) => ` ${a.name}: record.${a.name}?.toString() ?? null,`)
|
||||||
|
.join('\n')}\n${entity.attributes
|
||||||
|
.filter((a) => a.type === 'date')
|
||||||
|
.map((a) => ` ${a.name}: record.${a.name}?.toISOString() ?? null,`)
|
||||||
|
.join('\n')}\n };\n}\n\n@Injectable()\nexport class ${serviceName} {\n constructor(private readonly prisma: PrismaService) {}\n\n async findAll(query: { _start?: string; _end?: string; _sort?: string; _order?: string; [key: string]: any }) {\n const start = parseInt(query._start) || 0;\n const end = parseInt(query._end) || 10;\n const take = end - start;\n const skip = start;\n const sortField = query._sort || '${getBestSortField(entity, pk)}';\n const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';\n\n const where: any = {};\n\n if (query.q) {\n const q = String(query.q);\n const ors: any[] = [];\n ${entity.attributes
|
||||||
|
.filter((a) => ['string', 'text'].includes(a.type))
|
||||||
|
.slice(0, 6)
|
||||||
|
.map((a) => `ors.push({ ${a.name}: { contains: q, mode: 'insensitive' } });`)
|
||||||
|
.join('\n ')}\n if (ors.length) where.OR = ors;\n }\n\n ${entity.attributes
|
||||||
|
.filter((a) => ['string', 'text'].includes(a.type) && !a.foreign)
|
||||||
|
.map((a) => `if (query.${a.name}) where.${a.name} = { contains: query.${a.name}, mode: 'insensitive' };`)
|
||||||
|
.join('\n ')}\n\n ${entity.attributes
|
||||||
|
.filter((a) => a.foreign)
|
||||||
|
.map((a) => `if (query.${a.name}) where.${a.name} = query.${a.name};`)
|
||||||
|
.join('\n ')}\n\n // Enum multi-value support (e.g. status=A&status=B)\n ${entity.attributes
|
||||||
|
.filter((a) => !['string', 'text', 'uuid', 'integer', 'decimal', 'date'].includes(a.type))
|
||||||
|
.map((a) => `if (query.${a.name}) { const vals = Array.isArray(query.${a.name}) ? query.${a.name} : [query.${a.name}]; where.${a.name} = vals.length > 1 ? { in: vals } : vals[0]; }`)
|
||||||
|
.join('\n ')}\n\n if (query.id) {\n const ids = Array.isArray(query.id) ? query.id : [query.id];\n where.${pk} = { in: ids };\n }\n\n const [data, total] = await Promise.all([\n this.prisma.${lowerFirst(className)}.findMany({ where, skip, take, orderBy: { [sortField]: sortOrder } }),\n this.prisma.${lowerFirst(className)}.count({ where }),\n ]);\n\n const mapped = ${pk === 'id' ? 'data.map(serializeRecord)' : `data.map((r: any) => ({ id: r.${pk}, ...serializeRecord(r) }))`};\n return { data: mapped, total };\n }\n\n async findOne(id: string) {\n const record = await this.prisma.${lowerFirst(className)}.findUniqueOrThrow({ where: { ${pk}: id } as any });\n return ${pk === 'id' ? 'serializeRecord(record)' : `{ id: (record as any).${pk}, ...serializeRecord(record) }`};\n }\n\n async create(dto: Create${className}Dto) {\n const data: any = { ...(dto as any) };\n${entity.attributes
|
||||||
|
.filter((a) => a.type === 'date')
|
||||||
|
.map((a) => ` if (data.${a.name}) data.${a.name} = new Date(data.${a.name});`)
|
||||||
|
.join('\n')}\n${entity.attributes
|
||||||
|
.filter((a) => a.type === 'decimal')
|
||||||
|
.map((a) => ` if (data.${a.name}) data.${a.name} = new Prisma.Decimal(data.${a.name});`)
|
||||||
|
.join('\n')}\n\n const record = await this.prisma.${lowerFirst(className)}.create({ data });\n return ${pk === 'id' ? 'serializeRecord(record)' : `{ id: (record as any).${pk}, ...serializeRecord(record) }`};\n }\n\n async update(id: string, dto: Update${className}Dto) {\n const data: any = { ...(dto as any) };\n delete data.id;\n delete data.${pk};\n${entity.attributes
|
||||||
|
.filter((a) => a.type === 'date')
|
||||||
|
.map((a) => ` if (data.${a.name}) data.${a.name} = new Date(data.${a.name});`)
|
||||||
|
.join('\n')}\n${entity.attributes
|
||||||
|
.filter((a) => a.type === 'decimal')
|
||||||
|
.map((a) => ` if (data.${a.name} !== undefined && data.${a.name} !== null) data.${a.name} = new Prisma.Decimal(data.${a.name});`)
|
||||||
|
.join('\n')}\n\n const record = await this.prisma.${lowerFirst(className)}.update({ where: { ${pk}: id } as any, data });\n return ${pk === 'id' ? 'serializeRecord(record)' : `{ id: (record as any).${pk}, ...serializeRecord(record) }`};\n }\n\n async remove(id: string) {\n const record = await this.prisma.${lowerFirst(className)}.delete({ where: { ${pk}: id } as any });\n return ${pk === 'id' ? 'serializeRecord(record)' : `{ id: (record as any).${pk}, ...serializeRecord(record) }`};\n }\n}\n`;
|
||||||
|
|
||||||
|
let serviceContent = service
|
||||||
|
.replace(
|
||||||
|
`const sortField = query._sort || '${getBestSortField(entity, pk)}';\n const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';`,
|
||||||
|
`const sortField = query._sort || '${getBestSortField(entity, pk)}';\n const prismaSortField = sortField === 'id' ? '${pk}' : sortField;\n const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';`
|
||||||
|
)
|
||||||
|
.replace('orderBy: { [sortField]: sortOrder }', 'orderBy: { [prismaSortField]: sortOrder }')
|
||||||
|
.replace(
|
||||||
|
`data.map((r: any) => ({ id: r.${pk}, ...serializeRecord(r) }))`,
|
||||||
|
`data.map((item: any) => ({ id: item.${pk}, ...serializeRecord(item) }))`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pk !== 'id') {
|
||||||
|
serviceContent = serviceContent.replace(
|
||||||
|
`const data: any = { ...(dto as any) };\n delete data.id;\n delete data.${pk};`,
|
||||||
|
`const { id: _pk, ${pk}, ...rest } = (dto as any);\n const data: any = { ...rest };`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mod = `import { Module } from '@nestjs/common';\nimport { ${controllerName} } from './${folder}.controller';\nimport { ${serviceName} } from './${folder}.service';\n\n@Module({\n controllers: [${controllerName}],\n providers: [${serviceName}],\n})\nexport class ${moduleName} {}\n`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
folder,
|
||||||
|
files: {
|
||||||
|
[`server/src/modules/${folder}/${folder}.controller.ts`]: controller,
|
||||||
|
[`server/src/modules/${folder}/${folder}.service.ts`]: serviceContent,
|
||||||
|
[`server/src/modules/${folder}/${folder}.module.ts`]: mod,
|
||||||
|
[`server/src/modules/${folder}/dto/create-${folder}.dto.ts`]: createDtoLines.join('\n') + '\n',
|
||||||
|
[`server/src/modules/${folder}/dto/update-${folder}.dto.ts`]: updateDtoLines.join('\n') + '\n',
|
||||||
|
},
|
||||||
|
moduleName,
|
||||||
|
importPath: `./modules/${folder}/${folder}.module`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFrontendResource(entityName, entity, resourceName, pk, enums, allEntities) {
|
||||||
|
const folder = toKebab(entityName);
|
||||||
|
const className = entityName;
|
||||||
|
const enumAttrs = entity.attributes.filter(
|
||||||
|
(a) => !['string', 'text', 'uuid', 'integer', 'decimal', 'date'].includes(a.type)
|
||||||
|
);
|
||||||
|
const statusEnumAttr = enumAttrs.find((a) => a.name === 'status');
|
||||||
|
|
||||||
|
const identBase = toIdentifierFromKebab(folder);
|
||||||
|
const filtersIdent = `${identBase}Filters`;
|
||||||
|
const sortField = getBestSortField(entity, pk);
|
||||||
|
|
||||||
|
const hasNumber = entity.attributes.some((a) => ['integer', 'decimal'].includes(a.type));
|
||||||
|
const hasDate = entity.attributes.some((a) => a.type === 'date');
|
||||||
|
const hasFK = entity.attributes.some((a) => a.foreign);
|
||||||
|
const hasNonStatusEnum = enumAttrs.some((a) => a.name !== 'status');
|
||||||
|
|
||||||
|
const listImportSet = new Set([
|
||||||
|
'List',
|
||||||
|
'Datagrid',
|
||||||
|
'TextField',
|
||||||
|
'TextInput',
|
||||||
|
'TopToolbar',
|
||||||
|
'FilterButton',
|
||||||
|
'CreateButton',
|
||||||
|
'ExportButton',
|
||||||
|
]);
|
||||||
|
if (hasNumber) listImportSet.add('NumberField');
|
||||||
|
if (hasDate) listImportSet.add('DateField');
|
||||||
|
if (enumAttrs.length) listImportSet.add('SelectField');
|
||||||
|
if (hasFK) listImportSet.add('ReferenceField');
|
||||||
|
if (statusEnumAttr) listImportSet.add('SelectArrayInput');
|
||||||
|
if (hasNonStatusEnum) listImportSet.add('SelectInput');
|
||||||
|
if (hasFK) {
|
||||||
|
listImportSet.add('ReferenceInput');
|
||||||
|
listImportSet.add('AutocompleteInput');
|
||||||
|
}
|
||||||
|
const listImports = Array.from(listImportSet);
|
||||||
|
|
||||||
|
const choiceConsts = [];
|
||||||
|
for (const a of enumAttrs) {
|
||||||
|
const enumName = a.type;
|
||||||
|
const values = enums?.[enumName]?.values ?? [];
|
||||||
|
const labels = enums?.[enumName]?.labels ?? {};
|
||||||
|
const constName = `${a.name}Choices`;
|
||||||
|
if (a.name === 'status') {
|
||||||
|
choiceConsts.push(
|
||||||
|
`const statusChoices = [\n${values.map((v) => ` { id: '${v}', name: '${labels[v] ?? v}' },`).join('\n')}\n];\n`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
choiceConsts.push(
|
||||||
|
`const ${constName} = [\n${values.map((v) => ` { id: '${v}', name: '${labels[v] ?? v}' },`).join('\n')}\n];\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterInputs = [];
|
||||||
|
// Always include q if any string fields
|
||||||
|
if (entity.attributes.some((a) => ['string', 'text'].includes(a.type))) {
|
||||||
|
filterInputs.push(`<TextInput key="q" source="q" label="Поиск" alwaysOn />`);
|
||||||
|
}
|
||||||
|
for (const a of entity.attributes) {
|
||||||
|
const label = getAttributeLabel(a, allEntities);
|
||||||
|
if (a.name === pk) continue;
|
||||||
|
if (a.foreign) {
|
||||||
|
const referenceDisplay = getReferenceDisplayExpr(allEntities[a.foreign.entity]);
|
||||||
|
filterInputs.push(
|
||||||
|
`<ReferenceInput key="${a.name}" source="${a.name}" reference="${pluralize(toKebab(a.foreign.entity))}" label="${label}">\n <AutocompleteInput optionText={${referenceDisplay}} filterToQuery={(searchText) => ({ q: searchText })} />\n </ReferenceInput>`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (['string', 'text', 'uuid'].includes(a.type)) {
|
||||||
|
filterInputs.push(`<TextInput key="${a.name}" source="${a.name}" label="${label}" />`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (['integer', 'decimal'].includes(a.type)) continue;
|
||||||
|
if (a.type === 'date') continue;
|
||||||
|
// enum
|
||||||
|
if (a.name === 'status') {
|
||||||
|
filterInputs.push(`<SelectArrayInput key="${a.name}" source="${a.name}" label="${label}" choices={statusChoices} />`);
|
||||||
|
} else {
|
||||||
|
filterInputs.push(`<SelectInput key="${a.name}" source="${a.name}" label="${label}" choices={${a.name}Choices} emptyText="Все" />`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listFields = [];
|
||||||
|
for (const a of entity.attributes) {
|
||||||
|
const label = getAttributeLabel(a, allEntities);
|
||||||
|
if (a.foreign) {
|
||||||
|
const referenceEntity = allEntities[a.foreign.entity];
|
||||||
|
const referenceAttrs = getEntityAttrNames(referenceEntity);
|
||||||
|
const fieldSource = referenceAttrs.has('inventoryNumber')
|
||||||
|
? 'inventoryNumber'
|
||||||
|
: referenceAttrs.has('code')
|
||||||
|
? 'code'
|
||||||
|
: referenceAttrs.has('number')
|
||||||
|
? 'number'
|
||||||
|
: 'name';
|
||||||
|
listFields.push(
|
||||||
|
`<ReferenceField source="${a.name}" reference="${pluralize(toKebab(a.foreign.entity))}" label="${label}" link="show">\n <TextField source="${fieldSource}" />\n </ReferenceField>`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a.type === 'date') {
|
||||||
|
listFields.push(`<DateField source="${a.name}" label="${label}" />`);
|
||||||
|
} else if (['integer', 'decimal'].includes(a.type)) {
|
||||||
|
listFields.push(`<NumberField source="${a.name}" label="${label}" />`);
|
||||||
|
} else if (!['string', 'text', 'uuid', 'integer', 'decimal', 'date'].includes(a.type)) {
|
||||||
|
listFields.push(`<SelectField source="${a.name}" label="${label}" choices={${a.name === 'status' ? 'statusChoices' : `${a.name}Choices`}} />`);
|
||||||
|
} else {
|
||||||
|
listFields.push(`<TextField source="${a.name}" label="${label}" />`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = `import {\n ${listImports.join(',\n ')}\n} from 'react-admin';\n\n${choiceConsts.join('\n')}\nconst ${filtersIdent} = [\n ${filterInputs.join(',\n ')}\n];\n\nconst ${className}ListActions = () => (\n <TopToolbar>\n <FilterButton filters={${filtersIdent}} />\n <CreateButton />\n <ExportButton />\n </TopToolbar>\n);\n\nexport const ${className}List = () => (\n <List actions={<${className}ListActions />} filters={${filtersIdent}} sort={{ field: '${sortField}', order: 'ASC' }}>\n <Datagrid rowClick=\"show\">\n ${listFields.join('\n ')}\n </Datagrid>\n </List>\n);\n`;
|
||||||
|
|
||||||
|
const formField = (a, mode) => {
|
||||||
|
const label = getAttributeLabel(a, allEntities);
|
||||||
|
if (a.isPrimary && mode === 'create' && a.type === 'uuid') return null;
|
||||||
|
if (a.isPrimary && mode === 'edit') {
|
||||||
|
return `<TextInput source="${a.name}" label="${label}" disabled />`;
|
||||||
|
}
|
||||||
|
if (a.foreign) {
|
||||||
|
const referenceDisplay = getReferenceDisplayExpr(allEntities[a.foreign.entity]);
|
||||||
|
return `<ReferenceInput source="${a.name}" reference="${pluralize(toKebab(a.foreign.entity))}">\n <AutocompleteInput label="${label}" optionText={${referenceDisplay}} filterToQuery={(searchText) => ({ q: searchText })} />\n </ReferenceInput>`;
|
||||||
|
}
|
||||||
|
if (a.type === 'date') return `<DateInput source="${a.name}" label="${label}" />`;
|
||||||
|
if (['integer', 'decimal'].includes(a.type)) return `<NumberInput source="${a.name}" label="${label}" />`;
|
||||||
|
if (!['string', 'text', 'uuid', 'integer', 'decimal', 'date'].includes(a.type)) {
|
||||||
|
if (a.name === 'status' && statusEnumAttr) return `<SelectInput source="${a.name}" label="${label}" choices={statusChoices} emptyText="Не выбрано" />`;
|
||||||
|
return `<SelectInput source="${a.name}" label="${label}" choices={${a.name}Choices} emptyText="Не выбрано" />`;
|
||||||
|
}
|
||||||
|
return `<TextInput source="${a.name}" label="${label}" ${a.isRequired ? 'isRequired' : ''} />`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formImportSet = new Set(['SimpleForm', 'TextInput']);
|
||||||
|
if (hasNumber) formImportSet.add('NumberInput');
|
||||||
|
if (hasDate) formImportSet.add('DateInput');
|
||||||
|
if (enumAttrs.length) formImportSet.add('SelectInput');
|
||||||
|
if (hasFK) {
|
||||||
|
formImportSet.add('ReferenceInput');
|
||||||
|
formImportSet.add('AutocompleteInput');
|
||||||
|
}
|
||||||
|
const createImports = ['Create', ...Array.from(formImportSet)].join(', ');
|
||||||
|
const create = `import { ${createImports} } from 'react-admin';\n\n${choiceConsts.join('\n')}\nexport const ${className}Create = () => (\n <Create>\n <SimpleForm>\n ${entity.attributes.map((a) => formField(a, 'create')).filter(Boolean).join('\n ')}\n </SimpleForm>\n </Create>\n);\n`;
|
||||||
|
|
||||||
|
const editImports = ['Edit', ...Array.from(formImportSet)].join(', ');
|
||||||
|
const edit = `import { ${editImports} } from 'react-admin';\n\n${choiceConsts.join('\n')}\nexport const ${className}Edit = () => (\n <Edit>\n <SimpleForm>\n ${entity.attributes.map((a) => formField(a, 'edit')).filter(Boolean).join('\n ')}\n </SimpleForm>\n </Edit>\n);\n`;
|
||||||
|
|
||||||
|
const showImportSet = new Set(['Show', 'SimpleShowLayout', 'TextField']);
|
||||||
|
if (hasNumber) showImportSet.add('NumberField');
|
||||||
|
if (hasDate) showImportSet.add('DateField');
|
||||||
|
if (enumAttrs.length) showImportSet.add('SelectField');
|
||||||
|
if (hasFK) showImportSet.add('ReferenceField');
|
||||||
|
|
||||||
|
const showFields = [];
|
||||||
|
for (const a of entity.attributes) {
|
||||||
|
const label = getAttributeLabel(a, allEntities);
|
||||||
|
if (a.foreign) {
|
||||||
|
const referenceEntity = allEntities[a.foreign.entity];
|
||||||
|
const referenceAttrs = getEntityAttrNames(referenceEntity);
|
||||||
|
const fieldSource = referenceAttrs.has('inventoryNumber')
|
||||||
|
? 'inventoryNumber'
|
||||||
|
: referenceAttrs.has('code')
|
||||||
|
? 'code'
|
||||||
|
: referenceAttrs.has('number')
|
||||||
|
? 'number'
|
||||||
|
: 'name';
|
||||||
|
showFields.push(
|
||||||
|
`<ReferenceField source="${a.name}" reference="${pluralize(toKebab(a.foreign.entity))}" label="${label}" link="show">\n <TextField source="${fieldSource}" />\n </ReferenceField>`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a.type === 'date') {
|
||||||
|
showFields.push(`<DateField source="${a.name}" label="${label}" />`);
|
||||||
|
} else if (['integer', 'decimal'].includes(a.type)) {
|
||||||
|
showFields.push(`<NumberField source="${a.name}" label="${label}" />`);
|
||||||
|
} else if (!['string', 'text', 'uuid', 'integer', 'decimal', 'date'].includes(a.type)) {
|
||||||
|
showFields.push(`<SelectField source="${a.name}" label="${label}" choices={${a.name === 'status' ? 'statusChoices' : `${a.name}Choices`}} />`);
|
||||||
|
} else {
|
||||||
|
showFields.push(`<TextField source="${a.name}" label="${label}" />`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const show = `import { ${Array.from(showImportSet).join(', ')} } from 'react-admin';\n\n${choiceConsts.join('\n')}export const ${className}Show = () => (\n <Show>\n <SimpleShowLayout>\n ${showFields.join('\n ')}\n </SimpleShowLayout>\n </Show>\n);\n`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
files: {
|
||||||
|
[`client/src/resources/${folder}/${className}List.tsx`]: list,
|
||||||
|
[`client/src/resources/${folder}/${className}Create.tsx`]: create,
|
||||||
|
[`client/src/resources/${folder}/${className}Edit.tsx`]: edit,
|
||||||
|
[`client/src/resources/${folder}/${className}Show.tsx`]: show,
|
||||||
|
},
|
||||||
|
resourceName,
|
||||||
|
className,
|
||||||
|
folder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertInFile(filePath, apply, updater) {
|
||||||
|
const abs = path.join(ROOT, filePath);
|
||||||
|
const existing = fs.existsSync(abs) ? readFile(abs) : '';
|
||||||
|
const next = updater(existing);
|
||||||
|
if (apply) writeFile(abs, next);
|
||||||
|
return { changed: next !== existing, content: next };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureAppModule(apply, backendModules) {
|
||||||
|
return upsertInFile('server/src/app.module.ts', apply, (src) => {
|
||||||
|
let out = src;
|
||||||
|
for (const m of backendModules) {
|
||||||
|
if (!out.includes(`import { ${m.moduleName} }`)) {
|
||||||
|
const importLine = `import { ${m.moduleName} } from '${m.importPath}';`;
|
||||||
|
const importMatches = [...out.matchAll(/^import\s+.*;$/gm)];
|
||||||
|
if (importMatches.length) {
|
||||||
|
const lastImport = importMatches[importMatches.length - 1];
|
||||||
|
const insertAt = lastImport.index + lastImport[0].length;
|
||||||
|
out = `${out.slice(0, insertAt)}\n${importLine}${out.slice(insertAt)}`;
|
||||||
|
} else {
|
||||||
|
out = `${importLine}\n${out}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = out.replace(/imports:\s*\[\s*([\s\S]*?)\s*\],/m, (match, inner) => {
|
||||||
|
let block = inner;
|
||||||
|
for (const m of backendModules) {
|
||||||
|
if (!block.includes(m.moduleName)) block = block.replace(/\s*\],?\s*$/m, '') + `\n ${m.moduleName},`;
|
||||||
|
}
|
||||||
|
// normalize trailing comma/indent by reusing original replacement style
|
||||||
|
return `imports: [${block}\n ],`;
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureClientApp(apply, frontendResources) {
|
||||||
|
return upsertInFile('client/src/App.tsx', apply, (src) => {
|
||||||
|
let out = src;
|
||||||
|
for (const r of frontendResources) {
|
||||||
|
const imports = [
|
||||||
|
`import { ${r.className}List } from './resources/${r.folder}/${r.className}List';`,
|
||||||
|
`import { ${r.className}Create } from './resources/${r.folder}/${r.className}Create';`,
|
||||||
|
`import { ${r.className}Edit } from './resources/${r.folder}/${r.className}Edit';`,
|
||||||
|
`import { ${r.className}Show } from './resources/${r.folder}/${r.className}Show';`,
|
||||||
|
];
|
||||||
|
for (const imp of imports) {
|
||||||
|
if (!out.includes(imp)) {
|
||||||
|
const importMatches = [...out.matchAll(/^import\s+.*;$/gm)];
|
||||||
|
if (importMatches.length) {
|
||||||
|
const lastImport = importMatches[importMatches.length - 1];
|
||||||
|
const insertAt = lastImport.index + lastImport[0].length;
|
||||||
|
out = `${out.slice(0, insertAt)}\n${imports.join('\n')}${out.slice(insertAt)}`;
|
||||||
|
} else {
|
||||||
|
out = `${imports.join('\n')}\n${out}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!out.includes(`name="${r.resourceName}"`)) {
|
||||||
|
out = out.replace(
|
||||||
|
/<\/Admin>/m,
|
||||||
|
` <Resource\n name="${r.resourceName}"\n options={{ label: '${r.className}' }}\n list={${r.className}List}\n create={${r.className}Create}\n edit={${r.className}Edit}\n show={${r.className}Show}\n />\n </Admin>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const apply = args.includes('--apply');
|
||||||
|
const dslArgIdx = args.indexOf('--dsl');
|
||||||
|
const dslPath = dslArgIdx >= 0 ? args[dslArgIdx + 1] : 'domain/TOiR.domain.dsl';
|
||||||
|
|
||||||
|
const absDsl = path.resolve(ROOT, dslPath);
|
||||||
|
const dslText = readFile(absDsl);
|
||||||
|
const parsed = parseDomainDSL(dslText);
|
||||||
|
|
||||||
|
// Prisma schema
|
||||||
|
const prismaPath = path.join(ROOT, 'server/prisma/schema.prisma');
|
||||||
|
ensurePrismaSchema(parsed, prismaPath, apply);
|
||||||
|
|
||||||
|
// Backend modules + frontend resources
|
||||||
|
const backendModules = [];
|
||||||
|
const frontendResources = [];
|
||||||
|
for (const [entityName, ent] of Object.entries(parsed.entities)) {
|
||||||
|
const pk = ent.primaryKey;
|
||||||
|
const resource = pluralize(toKebab(entityName));
|
||||||
|
const be = renderBackendModule(entityName, ent, resource, pk);
|
||||||
|
const fe = renderFrontendResource(entityName, ent, resource, pk, parsed.enums, parsed.entities);
|
||||||
|
backendModules.push(be);
|
||||||
|
frontendResources.push(fe);
|
||||||
|
|
||||||
|
if (apply) {
|
||||||
|
for (const [rel, content] of Object.entries(be.files)) writeFile(path.join(ROOT, rel), content);
|
||||||
|
for (const [rel, content] of Object.entries(fe.files)) writeFile(path.join(ROOT, rel), content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureAppModule(apply, backendModules);
|
||||||
|
ensureClientApp(apply, frontendResources);
|
||||||
|
|
||||||
|
process.stdout.write(
|
||||||
|
`${apply ? 'Generated' : 'Planned'} ${Object.keys(parsed.entities).length} entities from ${dslPath}\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
||||||
@@ -56,6 +56,20 @@ The backend remains derived from `domain/*.dsl` inside the existing LLM-first pi
|
|||||||
- call `$connect()`
|
- call `$connect()`
|
||||||
- do not use `beforeExit`
|
- do not use `beforeExit`
|
||||||
|
|
||||||
|
## Filtering contract
|
||||||
|
|
||||||
|
- List endpoints must support React Admin query parameters:
|
||||||
|
- `_start`, `_end`, `_sort`, `_order`
|
||||||
|
- arbitrary field filters from query string
|
||||||
|
- `q` for reference autocomplete search
|
||||||
|
- String/text search filters may use `contains` with case-insensitive mode.
|
||||||
|
- Foreign key filters must use exact-match semantics (no `contains` for FK scalar keys).
|
||||||
|
- Enum filters must support both single and repeated query params:
|
||||||
|
- `status=Draft`
|
||||||
|
- `status=Draft&status=Approved`
|
||||||
|
- Repeated enum params must map to Prisma `{ in: [...] }`.
|
||||||
|
- Sorting must use real model scalar fields only; natural-key entities must not fallback to fake physical `id`.
|
||||||
|
|
||||||
## Reproducibility invariants
|
## Reproducibility invariants
|
||||||
|
|
||||||
- A freshly generated backend must be bootstrappable with ordinary Nest + Prisma commands from `prompts/runtime-rules.md`.
|
- A freshly generated backend must be bootstrappable with ordinary Nest + Prisma commands from `prompts/runtime-rules.md`.
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ The frontend stays a React Admin SPA generated from `domain/*.dsl` and anchored
|
|||||||
- Each entity becomes a React Admin resource with list/create/edit/show views.
|
- Each entity becomes a React Admin resource with list/create/edit/show views.
|
||||||
- Resource names must stay aligned with backend path segments.
|
- Resource names must stay aligned with backend path segments.
|
||||||
- Foreign keys must use `ReferenceInput` / `ReferenceField`.
|
- Foreign keys must use `ReferenceInput` / `ReferenceField`.
|
||||||
|
- Foreign keys shown in list/show views must stay clickable via `ReferenceField link="show"` to open full details of the related resource.
|
||||||
|
- Lists must expose filters through `List` `filters` and an actions toolbar with `FilterButton`.
|
||||||
|
- For enum fields where multi-select is required (for example `status`), use `SelectArrayInput` in list filters.
|
||||||
|
- For foreign key filters and form selection use `ReferenceInput` + `AutocompleteInput` with `filterToQuery={(searchText) => ({ q: searchText })}`.
|
||||||
|
- Form mapping must stay type-safe:
|
||||||
|
- `integer` / `decimal` -> `NumberInput`
|
||||||
|
- `date` -> `DateInput`
|
||||||
|
|
||||||
## Provider seams
|
## Provider seams
|
||||||
|
|
||||||
@@ -43,6 +50,8 @@ The frontend stays a React Admin SPA generated from `domain/*.dsl` and anchored
|
|||||||
- Every resource record must include `id`.
|
- Every resource record must include `id`.
|
||||||
- Natural-key resources must preserve route, update, and sort compatibility with React Admin contracts.
|
- Natural-key resources must preserve route, update, and sort compatibility with React Admin contracts.
|
||||||
- Frontend requests must continue to work when the real primary key is not named `id`.
|
- Frontend requests must continue to work when the real primary key is not named `id`.
|
||||||
|
- `dataProvider` query serialization must preserve repeated query params for array filters (for example enum multi-select).
|
||||||
|
- `Resource` wiring in `App.tsx` must keep `show={...}` registration for all generated resources.
|
||||||
|
|
||||||
## Reproducibility invariants
|
## Reproducibility invariants
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,19 @@ Validation is now a lightweight automated gate instead of a prose-only checklist
|
|||||||
2. OIDC discovery
|
2. OIDC discovery
|
||||||
3. certs fallback
|
3. certs fallback
|
||||||
|
|
||||||
|
### Filter checks
|
||||||
|
|
||||||
|
- list resources expose filter UI (including `FilterButton`)
|
||||||
|
- reference filters use `ReferenceInput` + `AutocompleteInput` with `filterToQuery`
|
||||||
|
- data provider preserves repeated query params for array filters
|
||||||
|
- backend FK filters keep exact-match semantics
|
||||||
|
- enum repeated params are mapped to Prisma `in`
|
||||||
|
- typed form mapping is preserved:
|
||||||
|
- `integer` / `decimal` -> `NumberInput`
|
||||||
|
- `date` -> `DateInput`
|
||||||
|
- reference fields intended for navigation keep `ReferenceField link="show"`
|
||||||
|
- resources keep `show={...}` registration in `App.tsx`
|
||||||
|
|
||||||
### Natural-key checks
|
### Natural-key checks
|
||||||
|
|
||||||
- response records expose `id`
|
- response records expose `id`
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"generate:from-dsl": "node ../generation/generate.mjs --apply --dsl domain/TOiR.domain.dsl",
|
||||||
"postinstall": "prisma generate"
|
"postinstall": "prisma generate"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "EquipmentStatus" AS ENUM ('Active', 'Repair', 'Reserve', 'WriteOff');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RepairKind" AS ENUM ('TO', 'TR', 'TRE', 'KR', 'AR', 'MP');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RepairOrderStatus" AS ENUM ('Draft', 'Approved', 'InWork', 'Done', 'Cancelled');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "EquipmentType" (
|
||||||
|
"code" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"manufacturer" TEXT,
|
||||||
|
"maintenanceIntervalHours" INTEGER,
|
||||||
|
"overhaulIntervalHours" INTEGER,
|
||||||
|
|
||||||
|
CONSTRAINT "EquipmentType_pkey" PRIMARY KEY ("code")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Equipment" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"inventoryNumber" TEXT NOT NULL,
|
||||||
|
"serialNumber" TEXT,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"equipmentTypeCode" TEXT NOT NULL,
|
||||||
|
"status" "EquipmentStatus" NOT NULL DEFAULT 'Active',
|
||||||
|
"location" TEXT,
|
||||||
|
"commissionedAt" TIMESTAMP(3),
|
||||||
|
"totalEngineHours" DECIMAL(65,30),
|
||||||
|
"engineHoursSinceLastRepair" DECIMAL(65,30),
|
||||||
|
"lastRepairAt" TIMESTAMP(3),
|
||||||
|
"notes" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Equipment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "RepairOrder" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"number" TEXT NOT NULL,
|
||||||
|
"equipmentId" TEXT NOT NULL,
|
||||||
|
"repairKind" "RepairKind" NOT NULL,
|
||||||
|
"status" "RepairOrderStatus" NOT NULL DEFAULT 'Draft',
|
||||||
|
"plannedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"startedAt" TIMESTAMP(3),
|
||||||
|
"completedAt" TIMESTAMP(3),
|
||||||
|
"contractor" TEXT,
|
||||||
|
"engineHoursAtRepair" DECIMAL(65,30),
|
||||||
|
"description" TEXT,
|
||||||
|
"notes" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "RepairOrder_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Equipment_inventoryNumber_key" ON "Equipment"("inventoryNumber");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "RepairOrder_number_key" ON "RepairOrder"("number");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Equipment" ADD CONSTRAINT "Equipment_equipmentTypeCode_fkey" FOREIGN KEY ("equipmentTypeCode") REFERENCES "EquipmentType"("code") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RepairOrder" ADD CONSTRAINT "RepairOrder_equipmentId_fkey" FOREIGN KEY ("equipmentId") REFERENCES "Equipment"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
@@ -32,43 +32,43 @@ enum RepairOrderStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model EquipmentType {
|
model EquipmentType {
|
||||||
code String @id
|
code String @id
|
||||||
name String
|
name String
|
||||||
manufacturer String?
|
manufacturer String?
|
||||||
maintenanceIntervalHours Int?
|
maintenanceIntervalHours Int?
|
||||||
overhaulIntervalHours Int?
|
overhaulIntervalHours Int?
|
||||||
equipment Equipment[]
|
equipment Equipment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Equipment {
|
model Equipment {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
inventoryNumber String @unique
|
inventoryNumber String @unique
|
||||||
serialNumber String?
|
serialNumber String?
|
||||||
name String
|
name String
|
||||||
equipmentTypeCode String
|
equipmentTypeCode String
|
||||||
equipmentType EquipmentType @relation(fields: [equipmentTypeCode], references: [code])
|
status EquipmentStatus @default(Active)
|
||||||
status EquipmentStatus @default(Active)
|
location String?
|
||||||
location String?
|
commissionedAt DateTime?
|
||||||
commissionedAt DateTime?
|
totalEngineHours Decimal?
|
||||||
totalEngineHours Decimal?
|
engineHoursSinceLastRepair Decimal?
|
||||||
engineHoursSinceLastRepair Decimal?
|
lastRepairAt DateTime?
|
||||||
lastRepairAt DateTime?
|
notes String?
|
||||||
notes String?
|
equipmentType EquipmentType @relation(fields: [equipmentTypeCode], references: [code])
|
||||||
repairOrders RepairOrder[]
|
repairOrders RepairOrder[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model RepairOrder {
|
model RepairOrder {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
number String @unique
|
number String @unique
|
||||||
equipmentId String
|
equipmentId String
|
||||||
equipment Equipment @relation(fields: [equipmentId], references: [id])
|
repairKind RepairKind
|
||||||
repairKind RepairKind
|
status RepairOrderStatus @default(Draft)
|
||||||
status RepairOrderStatus @default(Draft)
|
plannedAt DateTime
|
||||||
plannedAt DateTime
|
startedAt DateTime?
|
||||||
startedAt DateTime?
|
completedAt DateTime?
|
||||||
completedAt DateTime?
|
contractor String?
|
||||||
contractor String?
|
engineHoursAtRepair Decimal?
|
||||||
engineHoursAtRepair Decimal?
|
description String?
|
||||||
description String?
|
notes String?
|
||||||
notes String?
|
equipment Equipment @relation(fields: [equipmentId], references: [id])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,89 +3,161 @@ import { PrismaClient } from '@prisma/client';
|
|||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const equipmentType = await prisma.equipmentType.upsert({
|
const equipmentTypes = [
|
||||||
where: { code: 'pump' },
|
{
|
||||||
update: {},
|
|
||||||
create: {
|
|
||||||
code: 'pump',
|
code: 'pump',
|
||||||
name: 'Насосный агрегат',
|
name: 'Насосный агрегат',
|
||||||
manufacturer: 'АО НасосПром',
|
manufacturer: 'АО НасосПром',
|
||||||
maintenanceIntervalHours: 2000,
|
maintenanceIntervalHours: 2000,
|
||||||
overhaulIntervalHours: 16000,
|
overhaulIntervalHours: 16000,
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
|
||||||
const equipmentType2 = await prisma.equipmentType.upsert({
|
|
||||||
where: { code: 'compressor' },
|
|
||||||
update: {},
|
|
||||||
create: {
|
|
||||||
code: 'compressor',
|
code: 'compressor',
|
||||||
name: 'Компрессорная установка',
|
name: 'Компрессорная установка',
|
||||||
manufacturer: 'ОАО Компрессормаш',
|
manufacturer: 'ОАО Компрессормаш',
|
||||||
maintenanceIntervalHours: 1500,
|
maintenanceIntervalHours: 1500,
|
||||||
overhaulIntervalHours: 12000,
|
overhaulIntervalHours: 12000,
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
code: 'generator',
|
||||||
const equipment = await prisma.equipment.upsert({
|
name: 'Дизель-генератор',
|
||||||
where: { inventoryNumber: 'INV-001' },
|
manufacturer: 'АО ЭнергоМаш',
|
||||||
update: {},
|
maintenanceIntervalHours: 500,
|
||||||
create: {
|
overhaulIntervalHours: 6000,
|
||||||
inventoryNumber: 'INV-001',
|
|
||||||
serialNumber: 'SN-2024-0001',
|
|
||||||
name: 'Насос ЦНС 180-212',
|
|
||||||
equipmentTypeCode: 'pump',
|
|
||||||
status: 'Active',
|
|
||||||
location: 'Куст №5, скважина 42',
|
|
||||||
commissionedAt: new Date('2023-06-15'),
|
|
||||||
totalEngineHours: 4500,
|
|
||||||
engineHoursSinceLastRepair: 1200,
|
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
code: 'valve',
|
||||||
const equipment2 = await prisma.equipment.upsert({
|
name: 'Запорная арматура',
|
||||||
where: { inventoryNumber: 'INV-002' },
|
manufacturer: 'ЗАО АрматурПром',
|
||||||
update: {},
|
maintenanceIntervalHours: 1000,
|
||||||
create: {
|
overhaulIntervalHours: 10000,
|
||||||
inventoryNumber: 'INV-002',
|
|
||||||
serialNumber: 'SN-2024-0002',
|
|
||||||
name: 'Компрессор 4ВМ10-120/9',
|
|
||||||
equipmentTypeCode: 'compressor',
|
|
||||||
status: 'Active',
|
|
||||||
location: 'ГКС-3',
|
|
||||||
commissionedAt: new Date('2022-03-10'),
|
|
||||||
totalEngineHours: 8200,
|
|
||||||
engineHoursSinceLastRepair: 800,
|
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
code: 'sensor',
|
||||||
await prisma.repairOrder.upsert({
|
name: 'Датчик давления',
|
||||||
where: { number: 'RO-2026-001' },
|
manufacturer: 'ООО ПриборСервис',
|
||||||
update: {},
|
maintenanceIntervalHours: 800,
|
||||||
create: {
|
overhaulIntervalHours: 8000,
|
||||||
number: 'RO-2026-001',
|
|
||||||
equipmentId: equipment.id,
|
|
||||||
repairKind: 'TO',
|
|
||||||
status: 'Approved',
|
|
||||||
plannedAt: new Date('2026-04-01'),
|
|
||||||
contractor: 'ООО СервисРемонт',
|
|
||||||
engineHoursAtRepair: 4500,
|
|
||||||
description: 'Плановое техническое обслуживание насосного агрегата',
|
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
code: 'motor',
|
||||||
await prisma.repairOrder.upsert({
|
name: 'Электродвигатель',
|
||||||
where: { number: 'RO-2026-002' },
|
manufacturer: 'ПАО ЭлектроМотор',
|
||||||
update: {},
|
maintenanceIntervalHours: 1200,
|
||||||
create: {
|
overhaulIntervalHours: 14000,
|
||||||
number: 'RO-2026-002',
|
|
||||||
equipmentId: equipment2.id,
|
|
||||||
repairKind: 'TR',
|
|
||||||
status: 'Draft',
|
|
||||||
plannedAt: new Date('2026-05-15'),
|
|
||||||
description: 'Текущий ремонт компрессорной установки',
|
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
code: 'fan',
|
||||||
|
name: 'Вентилятор',
|
||||||
|
manufacturer: 'АО ВентПром',
|
||||||
|
maintenanceIntervalHours: 700,
|
||||||
|
overhaulIntervalHours: 9000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'heat-exchanger',
|
||||||
|
name: 'Теплообменник',
|
||||||
|
manufacturer: 'ОАО ТеплоТех',
|
||||||
|
maintenanceIntervalHours: 1800,
|
||||||
|
overhaulIntervalHours: 15000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'filter',
|
||||||
|
name: 'Фильтровальная установка',
|
||||||
|
manufacturer: 'ООО ФильтрТех',
|
||||||
|
maintenanceIntervalHours: 600,
|
||||||
|
overhaulIntervalHours: 7000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'separator',
|
||||||
|
name: 'Сепаратор',
|
||||||
|
manufacturer: 'АО СепараторМаш',
|
||||||
|
maintenanceIntervalHours: 1600,
|
||||||
|
overhaulIntervalHours: 13000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'transformer',
|
||||||
|
name: 'Трансформатор',
|
||||||
|
manufacturer: 'ПАО ТрансЭнерго',
|
||||||
|
maintenanceIntervalHours: 2500,
|
||||||
|
overhaulIntervalHours: 20000,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const type of equipmentTypes) {
|
||||||
|
await prisma.equipmentType.upsert({
|
||||||
|
where: { code: type.code },
|
||||||
|
update: { ...type },
|
||||||
|
create: { ...type },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const equipmentRecords: { id: string; inventoryNumber: string; name: string }[] = [];
|
||||||
|
for (let i = 1; i <= 11; i++) {
|
||||||
|
const type = equipmentTypes[(i - 1) % equipmentTypes.length];
|
||||||
|
const inventoryNumber = `INV-${String(i).padStart(3, '0')}`;
|
||||||
|
const serialNumber = `SN-2026-${String(i).padStart(4, '0')}`;
|
||||||
|
const record = await prisma.equipment.upsert({
|
||||||
|
where: { inventoryNumber },
|
||||||
|
update: {
|
||||||
|
serialNumber,
|
||||||
|
name: `${type.name} #${i}`,
|
||||||
|
equipmentTypeCode: type.code,
|
||||||
|
status: i % 5 === 0 ? 'Repair' : 'Active',
|
||||||
|
location: i % 2 === 0 ? `Площадка ${Math.ceil(i / 2)}` : `Цех ${Math.ceil(i / 3)}`,
|
||||||
|
commissionedAt: new Date(2022, (i - 1) % 12, 1 + ((i - 1) % 28)),
|
||||||
|
totalEngineHours: 1000 + i * 350,
|
||||||
|
engineHoursSinceLastRepair: 200 + i * 25,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
inventoryNumber,
|
||||||
|
serialNumber,
|
||||||
|
name: `${type.name} #${i}`,
|
||||||
|
equipmentTypeCode: type.code,
|
||||||
|
status: i % 5 === 0 ? 'Repair' : 'Active',
|
||||||
|
location: i % 2 === 0 ? `Площадка ${Math.ceil(i / 2)}` : `Цех ${Math.ceil(i / 3)}`,
|
||||||
|
commissionedAt: new Date(2022, (i - 1) % 12, 1 + ((i - 1) % 28)),
|
||||||
|
totalEngineHours: 1000 + i * 350,
|
||||||
|
engineHoursSinceLastRepair: 200 + i * 25,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
equipmentRecords.push({ id: record.id, inventoryNumber: record.inventoryNumber, name: record.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
const repairKinds = ['TO', 'TR', 'TRE', 'KR', 'AR', 'MP'] as const;
|
||||||
|
const statuses = ['Draft', 'Approved', 'InWork', 'Done', 'Cancelled'] as const;
|
||||||
|
|
||||||
|
for (let i = 1; i <= 11; i++) {
|
||||||
|
const number = `RO-2026-${String(i).padStart(3, '0')}`;
|
||||||
|
const equipment = equipmentRecords[(i - 1) % equipmentRecords.length];
|
||||||
|
await prisma.repairOrder.upsert({
|
||||||
|
where: { number },
|
||||||
|
update: {
|
||||||
|
equipmentId: equipment.id,
|
||||||
|
repairKind: repairKinds[(i - 1) % repairKinds.length],
|
||||||
|
status: statuses[(i - 1) % statuses.length],
|
||||||
|
plannedAt: new Date(2026, ((i - 1) % 12), 1 + ((i - 1) % 28)),
|
||||||
|
startedAt: i % 4 === 0 ? new Date(2026, ((i - 1) % 12), 2 + ((i - 1) % 28)) : null,
|
||||||
|
completedAt: i % 5 === 0 ? new Date(2026, ((i - 1) % 12), 5 + ((i - 1) % 28)) : null,
|
||||||
|
contractor: i % 3 === 0 ? 'ООО СервисРемонт' : 'АО ТехПодряд',
|
||||||
|
engineHoursAtRepair: 1000 + i * 350,
|
||||||
|
description: `Заявка на ремонт ${equipment.inventoryNumber} (${equipment.name})`,
|
||||||
|
notes: i % 2 === 0 ? 'Тестовая заметка' : null,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
number,
|
||||||
|
equipmentId: equipment.id,
|
||||||
|
repairKind: repairKinds[(i - 1) % repairKinds.length],
|
||||||
|
status: statuses[(i - 1) % statuses.length],
|
||||||
|
plannedAt: new Date(2026, ((i - 1) % 12), 1 + ((i - 1) % 28)),
|
||||||
|
startedAt: i % 4 === 0 ? new Date(2026, ((i - 1) % 12), 2 + ((i - 1) % 28)) : null,
|
||||||
|
completedAt: i % 5 === 0 ? new Date(2026, ((i - 1) % 12), 5 + ((i - 1) % 28)) : null,
|
||||||
|
contractor: i % 3 === 0 ? 'ООО СервисРемонт' : 'АО ТехПодряд',
|
||||||
|
engineHoursAtRepair: 1000 + i * 350,
|
||||||
|
description: `Заявка на ремонт ${equipment.inventoryNumber} (${equipment.name})`,
|
||||||
|
notes: i % 2 === 0 ? 'Тестовая заметка' : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Seed data created successfully');
|
console.log('Seed data created successfully');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import { EquipmentModule } from './modules/equipment/equipment.module';
|
|||||||
import { RepairOrderModule } from './modules/repair-order/repair-order.module';
|
import { RepairOrderModule } from './modules/repair-order/repair-order.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [ConfigModule.forRoot({
|
||||||
ConfigModule.forRoot({
|
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
validate: validateEnvironment,
|
validate: validateEnvironment,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export class CreateEquipmentTypeDto {
|
export class CreateEquipmentTypeDto {
|
||||||
code: string;
|
code?: string;
|
||||||
name: string;
|
name!: string;
|
||||||
manufacturer?: string;
|
manufacturer?: string;
|
||||||
maintenanceIntervalHours?: number;
|
maintenanceIntervalHours?: number;
|
||||||
overhaulIntervalHours?: number;
|
overhaulIntervalHours?: number;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export class UpdateEquipmentTypeDto {
|
export class UpdateEquipmentTypeDto {
|
||||||
|
id?: string;
|
||||||
|
code?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
manufacturer?: string;
|
manufacturer?: string;
|
||||||
maintenanceIntervalHours?: number;
|
maintenanceIntervalHours?: number;
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import { UpdateEquipmentTypeDto } from './dto/update-equipment-type.dto';
|
|||||||
|
|
||||||
@Controller('equipment-types')
|
@Controller('equipment-types')
|
||||||
export class EquipmentTypeController {
|
export class EquipmentTypeController {
|
||||||
constructor(private readonly equipmentTypeService: EquipmentTypeService) {}
|
constructor(private readonly service: EquipmentTypeService) {}
|
||||||
|
|
||||||
@Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)
|
@Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)
|
||||||
@Get()
|
@Get()
|
||||||
async findAll(@Query() query: any, @Res() res: Response) {
|
async findAll(@Query() query: any, @Res() res: Response) {
|
||||||
const result = await this.equipmentTypeService.findAll(query);
|
const result = await this.service.findAll(query);
|
||||||
res.set('Content-Range', `equipment-types ${query._start || 0}-${query._end || result.total}/${result.total}`);
|
res.set('Content-Range', `equipment-types ${query._start || 0}-${query._end || result.total}/${result.total}`);
|
||||||
res.set('Access-Control-Expose-Headers', 'Content-Range');
|
res.set('Access-Control-Expose-Headers', 'Content-Range');
|
||||||
return res.json(result.data);
|
return res.json(result.data);
|
||||||
@@ -21,25 +21,25 @@ export class EquipmentTypeController {
|
|||||||
|
|
||||||
@Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)
|
@Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)
|
||||||
@Get(':code')
|
@Get(':code')
|
||||||
findOne(@Param('code') code: string) {
|
findOne(@Param('code') id: string) {
|
||||||
return this.equipmentTypeService.findOne(code);
|
return this.service.findOne(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RealmRole.Editor, RealmRole.Admin)
|
@Roles(RealmRole.Editor, RealmRole.Admin)
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() dto: CreateEquipmentTypeDto) {
|
create(@Body() dto: CreateEquipmentTypeDto) {
|
||||||
return this.equipmentTypeService.create(dto);
|
return this.service.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RealmRole.Editor, RealmRole.Admin)
|
@Roles(RealmRole.Editor, RealmRole.Admin)
|
||||||
@Patch(':code')
|
@Patch(':code')
|
||||||
update(@Param('code') code: string, @Body() dto: UpdateEquipmentTypeDto) {
|
update(@Param('code') id: string, @Body() dto: UpdateEquipmentTypeDto) {
|
||||||
return this.equipmentTypeService.update(code, dto);
|
return this.service.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RealmRole.Admin)
|
@Roles(RealmRole.Admin)
|
||||||
@Delete(':code')
|
@Delete(':code')
|
||||||
remove(@Param('code') code: string) {
|
remove(@Param('code') id: string) {
|
||||||
return this.equipmentTypeService.remove(code);
|
return this.service.remove(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +1,90 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
import { PrismaService } from '../../prisma/prisma.service';
|
import { PrismaService } from '../../prisma/prisma.service';
|
||||||
import { CreateEquipmentTypeDto } from './dto/create-equipment-type.dto';
|
import { CreateEquipmentTypeDto } from './dto/create-equipment-type.dto';
|
||||||
import { UpdateEquipmentTypeDto } from './dto/update-equipment-type.dto';
|
import { UpdateEquipmentTypeDto } from './dto/update-equipment-type.dto';
|
||||||
|
|
||||||
|
function serializeRecord(record: any) {
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EquipmentTypeService {
|
export class EquipmentTypeService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
async findAll(query: {
|
async findAll(query: { _start?: string; _end?: string; _sort?: string; _order?: string; [key: string]: any }) {
|
||||||
_start?: string;
|
|
||||||
_end?: string;
|
|
||||||
_sort?: string;
|
|
||||||
_order?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}) {
|
|
||||||
const start = parseInt(query._start) || 0;
|
const start = parseInt(query._start) || 0;
|
||||||
const end = parseInt(query._end) || 10;
|
const end = parseInt(query._end) || 10;
|
||||||
const take = end - start;
|
const take = end - start;
|
||||||
const skip = start;
|
const skip = start;
|
||||||
const sortField = 'code';
|
const sortField = query._sort || 'code';
|
||||||
|
const prismaSortField = sortField === 'id' ? 'code' : sortField;
|
||||||
const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';
|
const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';
|
||||||
|
|
||||||
const where: any = {};
|
const where: any = {};
|
||||||
|
|
||||||
|
if (query.q) {
|
||||||
|
const q = String(query.q);
|
||||||
|
const ors: any[] = [];
|
||||||
|
ors.push({ code: { contains: q, mode: 'insensitive' } });
|
||||||
|
ors.push({ name: { contains: q, mode: 'insensitive' } });
|
||||||
|
ors.push({ manufacturer: { contains: q, mode: 'insensitive' } });
|
||||||
|
if (ors.length) where.OR = ors;
|
||||||
|
}
|
||||||
|
|
||||||
if (query.code) where.code = { contains: query.code, mode: 'insensitive' };
|
if (query.code) where.code = { contains: query.code, mode: 'insensitive' };
|
||||||
if (query.name) where.name = { contains: query.name, mode: 'insensitive' };
|
if (query.name) where.name = { contains: query.name, mode: 'insensitive' };
|
||||||
if (query.manufacturer)
|
if (query.manufacturer) where.manufacturer = { contains: query.manufacturer, mode: 'insensitive' };
|
||||||
where.manufacturer = {
|
|
||||||
contains: query.manufacturer,
|
|
||||||
mode: 'insensitive',
|
|
||||||
};
|
// Enum multi-value support (e.g. status=A&status=B)
|
||||||
|
|
||||||
|
|
||||||
if (query.id) {
|
if (query.id) {
|
||||||
const ids = Array.isArray(query.id) ? query.id : [query.id];
|
const ids = Array.isArray(query.id) ? query.id : [query.id];
|
||||||
where.code = { in: ids };
|
where.code = { in: ids };
|
||||||
}
|
}
|
||||||
|
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
this.prisma.equipmentType.findMany({
|
this.prisma.equipmentType.findMany({ where, skip, take, orderBy: { [prismaSortField]: sortOrder } }),
|
||||||
where,
|
|
||||||
skip,
|
|
||||||
take,
|
|
||||||
orderBy: { [sortField]: sortOrder },
|
|
||||||
}),
|
|
||||||
this.prisma.equipmentType.count({ where }),
|
this.prisma.equipmentType.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
const mapped = data.map((item: any) => ({ id: item.code, ...serializeRecord(item) }));
|
||||||
data: data.map((item) => ({ id: item.code, ...item })),
|
return { data: mapped, total };
|
||||||
total,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(code: string) {
|
async findOne(id: string) {
|
||||||
const record = await this.prisma.equipmentType.findUniqueOrThrow({
|
const record = await this.prisma.equipmentType.findUniqueOrThrow({ where: { code: id } as any });
|
||||||
where: { code },
|
return { id: (record as any).code, ...serializeRecord(record) };
|
||||||
});
|
|
||||||
return { id: record.code, ...record };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(dto: CreateEquipmentTypeDto) {
|
async create(dto: CreateEquipmentTypeDto) {
|
||||||
const record = await this.prisma.equipmentType.create({ data: dto });
|
const data: any = { ...(dto as any) };
|
||||||
return { id: record.code, ...record };
|
|
||||||
|
|
||||||
|
|
||||||
|
const record = await this.prisma.equipmentType.create({ data });
|
||||||
|
return { id: (record as any).code, ...serializeRecord(record) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(code: string, dto: UpdateEquipmentTypeDto) {
|
async update(id: string, dto: UpdateEquipmentTypeDto) {
|
||||||
const { id, code: _pk, ...data } = dto as any;
|
const { id: _pk, code, ...rest } = (dto as any);
|
||||||
const record = await this.prisma.equipmentType.update({
|
const data: any = { ...rest };
|
||||||
where: { code },
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
return { id: record.code, ...record };
|
const record = await this.prisma.equipmentType.update({ where: { code: id } as any, data });
|
||||||
|
return { id: (record as any).code, ...serializeRecord(record) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(code: string) {
|
async remove(id: string) {
|
||||||
const record = await this.prisma.equipmentType.delete({ where: { code } });
|
const record = await this.prisma.equipmentType.delete({ where: { code: id } as any });
|
||||||
return { id: record.code, ...record };
|
return { id: (record as any).code, ...serializeRecord(record) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export class CreateEquipmentDto {
|
export class CreateEquipmentDto {
|
||||||
inventoryNumber: string;
|
inventoryNumber!: string;
|
||||||
serialNumber?: string;
|
serialNumber?: string;
|
||||||
name: string;
|
name!: string;
|
||||||
equipmentTypeCode: string;
|
equipmentTypeCode!: string;
|
||||||
status?: string;
|
status!: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
commissionedAt?: string;
|
commissionedAt?: string;
|
||||||
totalEngineHours?: string;
|
totalEngineHours?: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export class UpdateEquipmentDto {
|
export class UpdateEquipmentDto {
|
||||||
|
id?: string;
|
||||||
inventoryNumber?: string;
|
inventoryNumber?: string;
|
||||||
serialNumber?: string;
|
serialNumber?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import { UpdateEquipmentDto } from './dto/update-equipment.dto';
|
|||||||
|
|
||||||
@Controller('equipment')
|
@Controller('equipment')
|
||||||
export class EquipmentController {
|
export class EquipmentController {
|
||||||
constructor(private readonly equipmentService: EquipmentService) {}
|
constructor(private readonly service: EquipmentService) {}
|
||||||
|
|
||||||
@Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)
|
@Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)
|
||||||
@Get()
|
@Get()
|
||||||
async findAll(@Query() query: any, @Res() res: Response) {
|
async findAll(@Query() query: any, @Res() res: Response) {
|
||||||
const result = await this.equipmentService.findAll(query);
|
const result = await this.service.findAll(query);
|
||||||
res.set('Content-Range', `equipment ${query._start || 0}-${query._end || result.total}/${result.total}`);
|
res.set('Content-Range', `equipment ${query._start || 0}-${query._end || result.total}/${result.total}`);
|
||||||
res.set('Access-Control-Expose-Headers', 'Content-Range');
|
res.set('Access-Control-Expose-Headers', 'Content-Range');
|
||||||
return res.json(result.data);
|
return res.json(result.data);
|
||||||
@@ -22,24 +22,24 @@ export class EquipmentController {
|
|||||||
@Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)
|
@Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id') id: string) {
|
||||||
return this.equipmentService.findOne(id);
|
return this.service.findOne(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RealmRole.Editor, RealmRole.Admin)
|
@Roles(RealmRole.Editor, RealmRole.Admin)
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() dto: CreateEquipmentDto) {
|
create(@Body() dto: CreateEquipmentDto) {
|
||||||
return this.equipmentService.create(dto);
|
return this.service.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RealmRole.Editor, RealmRole.Admin)
|
@Roles(RealmRole.Editor, RealmRole.Admin)
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
update(@Param('id') id: string, @Body() dto: UpdateEquipmentDto) {
|
update(@Param('id') id: string, @Body() dto: UpdateEquipmentDto) {
|
||||||
return this.equipmentService.update(id, dto);
|
return this.service.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RealmRole.Admin)
|
@Roles(RealmRole.Admin)
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
remove(@Param('id') id: string) {
|
remove(@Param('id') id: string) {
|
||||||
return this.equipmentService.remove(id);
|
return this.service.remove(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,66 +23,80 @@ export class EquipmentService {
|
|||||||
const end = parseInt(query._end) || 10;
|
const end = parseInt(query._end) || 10;
|
||||||
const take = end - start;
|
const take = end - start;
|
||||||
const skip = start;
|
const skip = start;
|
||||||
const sortField = query._sort || 'id';
|
const sortField = query._sort || 'inventoryNumber';
|
||||||
|
const prismaSortField = sortField === 'id' ? 'id' : sortField;
|
||||||
const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';
|
const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';
|
||||||
|
|
||||||
const where: any = {};
|
const where: any = {};
|
||||||
|
|
||||||
|
if (query.q) {
|
||||||
|
const q = String(query.q);
|
||||||
|
const ors: any[] = [];
|
||||||
|
ors.push({ inventoryNumber: { contains: q, mode: 'insensitive' } });
|
||||||
|
ors.push({ serialNumber: { contains: q, mode: 'insensitive' } });
|
||||||
|
ors.push({ name: { contains: q, mode: 'insensitive' } });
|
||||||
|
ors.push({ equipmentTypeCode: { contains: q, mode: 'insensitive' } });
|
||||||
|
ors.push({ location: { contains: q, mode: 'insensitive' } });
|
||||||
|
ors.push({ notes: { contains: q, mode: 'insensitive' } });
|
||||||
|
if (ors.length) where.OR = ors;
|
||||||
|
}
|
||||||
|
|
||||||
if (query.inventoryNumber) where.inventoryNumber = { contains: query.inventoryNumber, mode: 'insensitive' };
|
if (query.inventoryNumber) where.inventoryNumber = { contains: query.inventoryNumber, mode: 'insensitive' };
|
||||||
|
if (query.serialNumber) where.serialNumber = { contains: query.serialNumber, mode: 'insensitive' };
|
||||||
if (query.name) where.name = { contains: query.name, mode: 'insensitive' };
|
if (query.name) where.name = { contains: query.name, mode: 'insensitive' };
|
||||||
if (query.equipmentTypeCode) where.equipmentTypeCode = query.equipmentTypeCode;
|
|
||||||
if (query.status) where.status = query.status;
|
|
||||||
if (query.location) where.location = { contains: query.location, mode: 'insensitive' };
|
if (query.location) where.location = { contains: query.location, mode: 'insensitive' };
|
||||||
|
if (query.notes) where.notes = { contains: query.notes, mode: 'insensitive' };
|
||||||
|
|
||||||
|
if (query.equipmentTypeCode) where.equipmentTypeCode = query.equipmentTypeCode;
|
||||||
|
|
||||||
|
// Enum multi-value support (e.g. status=A&status=B)
|
||||||
|
if (query.status) { const vals = Array.isArray(query.status) ? query.status : [query.status]; where.status = vals.length > 1 ? { in: vals } : vals[0]; }
|
||||||
|
|
||||||
if (query.id) {
|
if (query.id) {
|
||||||
const ids = Array.isArray(query.id) ? query.id : [query.id];
|
const ids = Array.isArray(query.id) ? query.id : [query.id];
|
||||||
where.id = { in: ids };
|
where.id = { in: ids };
|
||||||
}
|
}
|
||||||
|
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
this.prisma.equipment.findMany({
|
this.prisma.equipment.findMany({ where, skip, take, orderBy: { [prismaSortField]: sortOrder } }),
|
||||||
where,
|
|
||||||
skip,
|
|
||||||
take,
|
|
||||||
orderBy: { [sortField]: sortOrder },
|
|
||||||
}),
|
|
||||||
this.prisma.equipment.count({ where }),
|
this.prisma.equipment.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
const mapped = data.map(serializeRecord);
|
||||||
data: data.map(serializeRecord),
|
return { data: mapped, total };
|
||||||
total,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: string) {
|
async findOne(id: string) {
|
||||||
const record = await this.prisma.equipment.findUniqueOrThrow({ where: { id } });
|
const record = await this.prisma.equipment.findUniqueOrThrow({ where: { id: id } as any });
|
||||||
return serializeRecord(record);
|
return serializeRecord(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(dto: CreateEquipmentDto) {
|
async create(dto: CreateEquipmentDto) {
|
||||||
const data: any = { ...dto };
|
const data: any = { ...(dto as any) };
|
||||||
if (dto.commissionedAt) data.commissionedAt = new Date(dto.commissionedAt);
|
if (data.commissionedAt) data.commissionedAt = new Date(data.commissionedAt);
|
||||||
if (dto.lastRepairAt) data.lastRepairAt = new Date(dto.lastRepairAt);
|
if (data.lastRepairAt) data.lastRepairAt = new Date(data.lastRepairAt);
|
||||||
if (dto.totalEngineHours) data.totalEngineHours = new Prisma.Decimal(dto.totalEngineHours);
|
if (data.totalEngineHours) data.totalEngineHours = new Prisma.Decimal(data.totalEngineHours);
|
||||||
if (dto.engineHoursSinceLastRepair) data.engineHoursSinceLastRepair = new Prisma.Decimal(dto.engineHoursSinceLastRepair);
|
if (data.engineHoursSinceLastRepair) data.engineHoursSinceLastRepair = new Prisma.Decimal(data.engineHoursSinceLastRepair);
|
||||||
|
|
||||||
const record = await this.prisma.equipment.create({ data });
|
const record = await this.prisma.equipment.create({ data });
|
||||||
return serializeRecord(record);
|
return serializeRecord(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, dto: UpdateEquipmentDto) {
|
async update(id: string, dto: UpdateEquipmentDto) {
|
||||||
const { id: _pk, ...rest } = dto as any;
|
const data: any = { ...(dto as any) };
|
||||||
const data: any = { ...rest };
|
delete data.id;
|
||||||
|
delete data.id;
|
||||||
if (data.commissionedAt) data.commissionedAt = new Date(data.commissionedAt);
|
if (data.commissionedAt) data.commissionedAt = new Date(data.commissionedAt);
|
||||||
if (data.lastRepairAt) data.lastRepairAt = new Date(data.lastRepairAt);
|
if (data.lastRepairAt) data.lastRepairAt = new Date(data.lastRepairAt);
|
||||||
if (data.totalEngineHours !== undefined && data.totalEngineHours !== null) data.totalEngineHours = new Prisma.Decimal(data.totalEngineHours);
|
if (data.totalEngineHours !== undefined && data.totalEngineHours !== null) data.totalEngineHours = new Prisma.Decimal(data.totalEngineHours);
|
||||||
if (data.engineHoursSinceLastRepair !== undefined && data.engineHoursSinceLastRepair !== null) data.engineHoursSinceLastRepair = new Prisma.Decimal(data.engineHoursSinceLastRepair);
|
if (data.engineHoursSinceLastRepair !== undefined && data.engineHoursSinceLastRepair !== null) data.engineHoursSinceLastRepair = new Prisma.Decimal(data.engineHoursSinceLastRepair);
|
||||||
|
|
||||||
const record = await this.prisma.equipment.update({ where: { id }, data });
|
const record = await this.prisma.equipment.update({ where: { id: id } as any, data });
|
||||||
return serializeRecord(record);
|
return serializeRecord(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(id: string) {
|
async remove(id: string) {
|
||||||
const record = await this.prisma.equipment.delete({ where: { id } });
|
const record = await this.prisma.equipment.delete({ where: { id: id } as any });
|
||||||
return serializeRecord(record);
|
return serializeRecord(record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export class CreateRepairOrderDto {
|
export class CreateRepairOrderDto {
|
||||||
number: string;
|
number!: string;
|
||||||
equipmentId: string;
|
equipmentId!: string;
|
||||||
repairKind: string;
|
repairKind!: string;
|
||||||
status?: string;
|
status!: string;
|
||||||
plannedAt: string;
|
plannedAt!: string;
|
||||||
startedAt?: string;
|
startedAt?: string;
|
||||||
completedAt?: string;
|
completedAt?: string;
|
||||||
contractor?: string;
|
contractor?: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export class UpdateRepairOrderDto {
|
export class UpdateRepairOrderDto {
|
||||||
|
id?: string;
|
||||||
number?: string;
|
number?: string;
|
||||||
equipmentId?: string;
|
equipmentId?: string;
|
||||||
repairKind?: string;
|
repairKind?: string;
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import { UpdateRepairOrderDto } from './dto/update-repair-order.dto';
|
|||||||
|
|
||||||
@Controller('repair-orders')
|
@Controller('repair-orders')
|
||||||
export class RepairOrderController {
|
export class RepairOrderController {
|
||||||
constructor(private readonly repairOrderService: RepairOrderService) {}
|
constructor(private readonly service: RepairOrderService) {}
|
||||||
|
|
||||||
@Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)
|
@Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)
|
||||||
@Get()
|
@Get()
|
||||||
async findAll(@Query() query: any, @Res() res: Response) {
|
async findAll(@Query() query: any, @Res() res: Response) {
|
||||||
const result = await this.repairOrderService.findAll(query);
|
const result = await this.service.findAll(query);
|
||||||
res.set('Content-Range', `repair-orders ${query._start || 0}-${query._end || result.total}/${result.total}`);
|
res.set('Content-Range', `repair-orders ${query._start || 0}-${query._end || result.total}/${result.total}`);
|
||||||
res.set('Access-Control-Expose-Headers', 'Content-Range');
|
res.set('Access-Control-Expose-Headers', 'Content-Range');
|
||||||
return res.json(result.data);
|
return res.json(result.data);
|
||||||
@@ -22,24 +22,24 @@ export class RepairOrderController {
|
|||||||
@Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)
|
@Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id') id: string) {
|
||||||
return this.repairOrderService.findOne(id);
|
return this.service.findOne(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RealmRole.Editor, RealmRole.Admin)
|
@Roles(RealmRole.Editor, RealmRole.Admin)
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() dto: CreateRepairOrderDto) {
|
create(@Body() dto: CreateRepairOrderDto) {
|
||||||
return this.repairOrderService.create(dto);
|
return this.service.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RealmRole.Editor, RealmRole.Admin)
|
@Roles(RealmRole.Editor, RealmRole.Admin)
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
update(@Param('id') id: string, @Body() dto: UpdateRepairOrderDto) {
|
update(@Param('id') id: string, @Body() dto: UpdateRepairOrderDto) {
|
||||||
return this.repairOrderService.update(id, dto);
|
return this.service.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RealmRole.Admin)
|
@Roles(RealmRole.Admin)
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
remove(@Param('id') id: string) {
|
remove(@Param('id') id: string) {
|
||||||
return this.repairOrderService.remove(id);
|
return this.service.remove(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,66 +23,78 @@ export class RepairOrderService {
|
|||||||
const end = parseInt(query._end) || 10;
|
const end = parseInt(query._end) || 10;
|
||||||
const take = end - start;
|
const take = end - start;
|
||||||
const skip = start;
|
const skip = start;
|
||||||
const sortField = query._sort || 'id';
|
const sortField = query._sort || 'number';
|
||||||
|
const prismaSortField = sortField === 'id' ? 'id' : sortField;
|
||||||
const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';
|
const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';
|
||||||
|
|
||||||
const where: any = {};
|
const where: any = {};
|
||||||
|
|
||||||
|
if (query.q) {
|
||||||
|
const q = String(query.q);
|
||||||
|
const ors: any[] = [];
|
||||||
|
ors.push({ number: { contains: q, mode: 'insensitive' } });
|
||||||
|
ors.push({ contractor: { contains: q, mode: 'insensitive' } });
|
||||||
|
ors.push({ description: { contains: q, mode: 'insensitive' } });
|
||||||
|
ors.push({ notes: { contains: q, mode: 'insensitive' } });
|
||||||
|
if (ors.length) where.OR = ors;
|
||||||
|
}
|
||||||
|
|
||||||
if (query.number) where.number = { contains: query.number, mode: 'insensitive' };
|
if (query.number) where.number = { contains: query.number, mode: 'insensitive' };
|
||||||
if (query.equipmentId) where.equipmentId = query.equipmentId;
|
|
||||||
if (query.repairKind) where.repairKind = query.repairKind;
|
|
||||||
if (query.status) where.status = query.status;
|
|
||||||
if (query.contractor) where.contractor = { contains: query.contractor, mode: 'insensitive' };
|
if (query.contractor) where.contractor = { contains: query.contractor, mode: 'insensitive' };
|
||||||
|
if (query.description) where.description = { contains: query.description, mode: 'insensitive' };
|
||||||
|
if (query.notes) where.notes = { contains: query.notes, mode: 'insensitive' };
|
||||||
|
|
||||||
|
if (query.equipmentId) where.equipmentId = query.equipmentId;
|
||||||
|
|
||||||
|
// Enum multi-value support (e.g. status=A&status=B)
|
||||||
|
if (query.repairKind) { const vals = Array.isArray(query.repairKind) ? query.repairKind : [query.repairKind]; where.repairKind = vals.length > 1 ? { in: vals } : vals[0]; }
|
||||||
|
if (query.status) { const vals = Array.isArray(query.status) ? query.status : [query.status]; where.status = vals.length > 1 ? { in: vals } : vals[0]; }
|
||||||
|
|
||||||
if (query.id) {
|
if (query.id) {
|
||||||
const ids = Array.isArray(query.id) ? query.id : [query.id];
|
const ids = Array.isArray(query.id) ? query.id : [query.id];
|
||||||
where.id = { in: ids };
|
where.id = { in: ids };
|
||||||
}
|
}
|
||||||
|
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
this.prisma.repairOrder.findMany({
|
this.prisma.repairOrder.findMany({ where, skip, take, orderBy: { [prismaSortField]: sortOrder } }),
|
||||||
where,
|
|
||||||
skip,
|
|
||||||
take,
|
|
||||||
orderBy: { [sortField]: sortOrder },
|
|
||||||
}),
|
|
||||||
this.prisma.repairOrder.count({ where }),
|
this.prisma.repairOrder.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
const mapped = data.map(serializeRecord);
|
||||||
data: data.map(serializeRecord),
|
return { data: mapped, total };
|
||||||
total,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: string) {
|
async findOne(id: string) {
|
||||||
const record = await this.prisma.repairOrder.findUniqueOrThrow({ where: { id } });
|
const record = await this.prisma.repairOrder.findUniqueOrThrow({ where: { id: id } as any });
|
||||||
return serializeRecord(record);
|
return serializeRecord(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(dto: CreateRepairOrderDto) {
|
async create(dto: CreateRepairOrderDto) {
|
||||||
const data: any = { ...dto };
|
const data: any = { ...(dto as any) };
|
||||||
if (dto.plannedAt) data.plannedAt = new Date(dto.plannedAt);
|
if (data.plannedAt) data.plannedAt = new Date(data.plannedAt);
|
||||||
if (dto.startedAt) data.startedAt = new Date(dto.startedAt);
|
if (data.startedAt) data.startedAt = new Date(data.startedAt);
|
||||||
if (dto.completedAt) data.completedAt = new Date(dto.completedAt);
|
if (data.completedAt) data.completedAt = new Date(data.completedAt);
|
||||||
if (dto.engineHoursAtRepair) data.engineHoursAtRepair = new Prisma.Decimal(dto.engineHoursAtRepair);
|
if (data.engineHoursAtRepair) data.engineHoursAtRepair = new Prisma.Decimal(data.engineHoursAtRepair);
|
||||||
|
|
||||||
const record = await this.prisma.repairOrder.create({ data });
|
const record = await this.prisma.repairOrder.create({ data });
|
||||||
return serializeRecord(record);
|
return serializeRecord(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, dto: UpdateRepairOrderDto) {
|
async update(id: string, dto: UpdateRepairOrderDto) {
|
||||||
const { id: _pk, ...rest } = dto as any;
|
const data: any = { ...(dto as any) };
|
||||||
const data: any = { ...rest };
|
delete data.id;
|
||||||
|
delete data.id;
|
||||||
if (data.plannedAt) data.plannedAt = new Date(data.plannedAt);
|
if (data.plannedAt) data.plannedAt = new Date(data.plannedAt);
|
||||||
if (data.startedAt) data.startedAt = new Date(data.startedAt);
|
if (data.startedAt) data.startedAt = new Date(data.startedAt);
|
||||||
if (data.completedAt) data.completedAt = new Date(data.completedAt);
|
if (data.completedAt) data.completedAt = new Date(data.completedAt);
|
||||||
if (data.engineHoursAtRepair !== undefined && data.engineHoursAtRepair !== null) data.engineHoursAtRepair = new Prisma.Decimal(data.engineHoursAtRepair);
|
if (data.engineHoursAtRepair !== undefined && data.engineHoursAtRepair !== null) data.engineHoursAtRepair = new Prisma.Decimal(data.engineHoursAtRepair);
|
||||||
|
|
||||||
const record = await this.prisma.repairOrder.update({ where: { id }, data });
|
const record = await this.prisma.repairOrder.update({ where: { id: id } as any, data });
|
||||||
return serializeRecord(record);
|
return serializeRecord(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(id: string) {
|
async remove(id: string) {
|
||||||
const record = await this.prisma.repairOrder.delete({ where: { id } });
|
const record = await this.prisma.repairOrder.delete({ where: { id: id } as any });
|
||||||
return serializeRecord(record);
|
return serializeRecord(record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user