fuel; nest api

This commit is contained in:
2025-12-23 09:53:04 +09:00
parent fa516b3a20
commit 04ce74d320
23 changed files with 1125 additions and 183 deletions

View File

@ -17,7 +17,7 @@ const ObjectTree = ({
const { data: existingObjectsList } = useSWR( const { data: existingObjectsList } = useSWR(
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=0` : null, selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=0` : null,
(url) => fetcher(url, BASE_URL.ems).then(res => { (url) => fetcher(url, BASE_URL.nest).then(res => {
if (Array.isArray(res)) { if (Array.isArray(res)) {
let count = 0 let count = 0
res.forEach(el => { res.forEach(el => {
@ -34,7 +34,7 @@ const ObjectTree = ({
const { data: planningObjectsList } = useSWR( const { data: planningObjectsList } = useSWR(
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=1` : null, selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=1` : null,
(url) => fetcher(url, BASE_URL.ems).then(res => { (url) => fetcher(url, BASE_URL.nest).then(res => {
if (Array.isArray(res)) { if (Array.isArray(res)) {
let count = 0 let count = 0
res.forEach(el => { res.forEach(el => {
@ -116,7 +116,7 @@ const ObjectList = ({
const { data } = useSWR( const { data } = useSWR(
selectedDistrict && selectedYear ? `/general/objects/list?type=${id}&city_id=${selectedDistrict}&year=${selectedYear}&planning=${planning}` : null, selectedDistrict && selectedYear ? `/general/objects/list?type=${id}&city_id=${selectedDistrict}&year=${selectedYear}&planning=${planning}` : null,
(url) => fetcher(url, BASE_URL.ems), (url) => fetcher(url, BASE_URL.nest),
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateIfStale: false revalidateIfStale: false

View File

@ -78,20 +78,20 @@ const MapComponent = ({
const mapElement = useRef<HTMLDivElement | null>(null) const mapElement = useRef<HTMLDivElement | null>(null)
// Get type roles // Get type roles
useSWR(`/gis/type-roles`, (url) => fetcher(url, BASE_URL.ems).then(res => { useSWR(`/gis/type-roles`, (url) => fetcher(url, BASE_URL.nest).then(res => {
if (Array.isArray(res)) { if (Array.isArray(res)) {
setTypeRoles(id, res) setTypeRoles(id, res)
} }
return res return res
}), swrOptions) }), swrOptions)
const { data: regionsData } = useSWR(`/general/regions`, (url) => fetcher(url, BASE_URL.ems).then(res => { const { data: regionsData } = useSWR(`/general/regions`, (url) => fetcher(url, BASE_URL.nest).then(res => {
setRegionsData(res) setRegionsData(res)
return res return res
}), swrOptions) }), swrOptions)
// Bounds: region // Bounds: region
const { data: regionBoundsData } = useSWR(`/gis/bounds/region`, (url) => fetcher(url, BASE_URL.ems), swrOptions) const { data: regionBoundsData } = useSWR(`/gis/bounds/region`, (url) => fetcher(url, BASE_URL.nest), swrOptions)
// Map init // Map init
useEffect(() => { useEffect(() => {
@ -169,7 +169,7 @@ const MapComponent = ({
useEffect(() => { useEffect(() => {
if (selectedDistrict && selectedYear && districtBoundLayer) { if (selectedDistrict && selectedYear && districtBoundLayer) {
const bounds = new VectorSource({ const bounds = new VectorSource({
url: `${BASE_URL.ems}/gis/bounds/city/${selectedDistrict}`, url: `${BASE_URL.nest}/gis/bounds/city/${selectedDistrict}`,
format: new GeoJSON(), format: new GeoJSON(),
}) })
@ -279,7 +279,7 @@ const MapComponent = ({
} }
}, [currentObjectId, figuresLayer, linesLayer]) }, [currentObjectId, figuresLayer, linesLayer])
const { data: districtsData } = useSWR(selectedRegion ? `/general/districts/?region_id=${selectedRegion}` : null, (url) => fetcher(url, BASE_URL.ems).then(res => { const { data: districtsData } = useSWR(selectedRegion ? `/general/districts/?region_id=${selectedRegion}` : null, (url) => fetcher(url, BASE_URL.nest).then(res => {
setDistrictsData(res) setDistrictsData(res)
return res return res
}), swrOptions) }), swrOptions)
@ -287,7 +287,7 @@ const MapComponent = ({
const { data: figuresData, isValidating: figuresValidating } = useSWR( const { data: figuresData, isValidating: figuresValidating } = useSWR(
selectedDistrict && selectedYear ? `/gis/figures/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null, selectedDistrict && selectedYear ? `/gis/figures/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null,
(url) => axios.get(url, { (url) => axios.get(url, {
baseURL: BASE_URL.ems baseURL: BASE_URL.nest
}).then((res) => res.data), }).then((res) => res.data),
swrOptions swrOptions
) )
@ -295,7 +295,7 @@ const MapComponent = ({
const { data: linesData, isValidating: linesValidating } = useSWR( const { data: linesData, isValidating: linesValidating } = useSWR(
!figuresValidating && selectedDistrict && selectedYear ? `/gis/lines/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null, !figuresValidating && selectedDistrict && selectedYear ? `/gis/lines/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null,
(url) => axios.get(url, { (url) => axios.get(url, {
baseURL: BASE_URL.ems baseURL: BASE_URL.nest
}).then((res) => res.data), }).then((res) => res.data),
swrOptions swrOptions
) )
@ -303,7 +303,7 @@ const MapComponent = ({
const { data: districtData } = useSWR( const { data: districtData } = useSWR(
selectedDistrict ? `/gis/images/all?city_id=${selectedDistrict}` : null, selectedDistrict ? `/gis/images/all?city_id=${selectedDistrict}` : null,
(url) => axios.get(url, { (url) => axios.get(url, {
baseURL: BASE_URL.ems baseURL: BASE_URL.nest
}).then((res) => Array.isArray(res.data) ? res.data[0] : null), }).then((res) => Array.isArray(res.data) ? res.data[0] : null),
swrOptions swrOptions
) )
@ -398,7 +398,7 @@ const MapComponent = ({
list.push(district.id as Number) list.push(district.id as Number)
}) })
fetch(`${BASE_URL.ems}/gis/bounds/city`, { fetch(`${BASE_URL.nest}/gis/bounds/city`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -15,7 +15,7 @@ const MapLegend = ({
const { data: existingObjectsList } = useSWR( const { data: existingObjectsList } = useSWR(
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=0` : null, selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=0` : null,
(url) => fetcher(url, BASE_URL.ems), (url) => fetcher(url, BASE_URL.nest),
{ {
revalidateOnFocus: false revalidateOnFocus: false
} }
@ -23,7 +23,7 @@ const MapLegend = ({
const { data: planningObjectsList } = useSWR( const { data: planningObjectsList } = useSWR(
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=1` : null, selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=1` : null,
(url) => fetcher(url, BASE_URL.ems), (url) => fetcher(url, BASE_URL.nest),
{ {
revalidateOnFocus: false revalidateOnFocus: false
} }

View File

@ -22,7 +22,7 @@ const MapObjectSearch = ({
const { data: searchData } = useSWR( const { data: searchData } = useSWR(
throttledSearchObject !== "" && selectedDistrict && selectedYear ? `/general/search/objects?q=${throttledSearchObject}&id_city=${selectedDistrict}&year=${selectedYear}` : null, throttledSearchObject !== "" && selectedDistrict && selectedYear ? `/general/search/objects?q=${throttledSearchObject}&id_city=${selectedDistrict}&year=${selectedYear}` : null,
(url) => fetcher(url, BASE_URL.ems), (url) => fetcher(url, BASE_URL.nest),
swrOptions swrOptions
) )

View File

@ -25,7 +25,7 @@ const MapRegionSelect = ({
const { selectedRegion, selectedYear, selectedDistrict } = useObjectsStore().id[map_id] const { selectedRegion, selectedYear, selectedDistrict } = useObjectsStore().id[map_id]
const { regionsData } = useRegionsStore() const { regionsData } = useRegionsStore()
const { data: districtsData } = useSWR(selectedRegion ? `/general/districts/?region_id=${selectedRegion}` : null, (url) => fetcher(url, BASE_URL.ems).then(res => { const { data: districtsData } = useSWR(selectedRegion ? `/general/districts/?region_id=${selectedRegion}` : null, (url) => fetcher(url, BASE_URL.nest).then(res => {
setDistrictsData(res) setDistrictsData(res)
return res return res
}), swrOptions) }), swrOptions)

View File

@ -6,7 +6,7 @@ import { BASE_URL } from '../../constants'
const ObjectData = (object_data: IObjectData) => { const ObjectData = (object_data: IObjectData) => {
const { data: typeData } = useSWR( const { data: typeData } = useSWR(
object_data.type ? `/general/types` : null, object_data.type ? `/general/types` : null,
(url) => fetcher(url, BASE_URL.ems), (url) => fetcher(url, BASE_URL.nest),
{ {
revalidateOnFocus: false revalidateOnFocus: false
} }

View File

@ -17,7 +17,7 @@ const ObjectParameters = ({
const { data: valuesData, isValidating: valuesValidating } = useSWR( const { data: valuesData, isValidating: valuesValidating } = useSWR(
currentObjectId ? `/general/values/?object_id=${currentObjectId}` : null, currentObjectId ? `/general/values/?object_id=${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems), (url) => fetcher(url, BASE_URL.nest),
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateIfStale: false revalidateIfStale: false

View File

@ -3,7 +3,7 @@ import { BASE_URL } from '../../constants'
import { fetcher } from '../../http/axiosInstance' import { fetcher } from '../../http/axiosInstance'
const RegionSelect = () => { const RegionSelect = () => {
const { data } = useSWR(`/gis/regions/borders`, (url) => fetcher(url, BASE_URL.ems), { const { data } = useSWR(`/gis/regions/borders`, (url) => fetcher(url, BASE_URL.nest), {
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateIfStale: false revalidateIfStale: false
}) })

View File

@ -30,7 +30,7 @@ const TableValue = ({
const { data: tcbAll, isValidating } = useSWR( const { data: tcbAll, isValidating } = useSWR(
type === 'select' && selectedDistrict ? `/general/params/tcb?vtable=${vtable}&id_city=${selectedDistrict}` : null, type === 'select' && selectedDistrict ? `/general/params/tcb?vtable=${vtable}&id_city=${selectedDistrict}` : null,
(url) => fetcher(url, BASE_URL.ems).then(res => { (url) => fetcher(url, BASE_URL.nest).then(res => {
if (Array.isArray(res)) { if (Array.isArray(res)) {
return res.map((el) => ({ return res.map((el) => ({
label: el.name || "", label: el.name || "",

View File

@ -9,4 +9,5 @@ export const BASE_URL = {
fuel: import.meta.env.VITE_API_FUEL_URL, fuel: import.meta.env.VITE_API_FUEL_URL,
servers: import.meta.env.VITE_API_SERVERS_URL, servers: import.meta.env.VITE_API_SERVERS_URL,
ems: import.meta.env.VITE_API_EMS_URL, ems: import.meta.env.VITE_API_EMS_URL,
nest: import.meta.env.VITE_API_NEST_URL
} }

View File

@ -3,6 +3,11 @@ export interface IRegion {
name: string; name: string;
} }
export interface ISeason {
name: string
year: number
}
export interface ICity { export interface ICity {
id: number; id: number;
name: string; name: string;

View File

@ -1,49 +1,117 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Combobox, CompoundButton, Option, Text } from '@fluentui/react-components' import { Dropdown, Option, Spinner } from '@fluentui/react-components'
import useSWR from 'swr' import useSWR from 'swr'
import axios from 'axios' import axios from 'axios'
import { AgGridReact, CustomCellRendererProps } from 'ag-grid-react' import { AgGridReact, CustomCellRendererProps } from 'ag-grid-react'
import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community' import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community'
import FuelRenderer from './FuelRenderer' import FuelRenderer from './FuelRenderer'
import { useSearchParams } from 'react-router-dom'
import { ICity, IRegion } from '../../../interfaces/fuel'
import BoilersCard from './BoilersCard'
ModuleRegistry.registerModules([AllCommunityModule]) ModuleRegistry.registerModules([AllCommunityModule])
function Boilers() { function Boilers() {
const [, setBoilersPage] = useState(1) const [searchParams, setSearchParams] = useSearchParams();
const [boilerSearch, setBoilerSearch] = useState("")
const [, setDebouncedBoilerSearch] = useState("")
// const { boilers } = useBoilers(10, boilersPage, debouncedBoilerSearch)
const { data: regions } = useSWR('/general/regions', () => axios.get('/general/regions', { const [regionId, setRegionId] = useState<number | undefined>(
baseURL: import.meta.env.VITE_API_NEST_URL searchParams.get("region_id") ? Number(searchParams.get("region_id")) : undefined
}).then(res => res.data)) );
const [regionId, setRegionId] = useState<number | undefined>(undefined) const [cityId, setCityId] = useState<number | undefined>(
searchParams.get("city_id") ? Number(searchParams.get("city_id")) : undefined
)
const { data: cities } = useSWR(regionId ? `/general/cities?region_id=${regionId}` : null, () => axios.get(`/general/cities?region_id=${regionId}`, { // Load regions
baseURL: import.meta.env.VITE_API_NEST_URL const { data: regions, isLoading: regionsLoading } = useSWR("/general/regions", () =>
}).then(res => res.data)) axios
.get("/general/regions", { baseURL: import.meta.env.VITE_API_NEST_URL })
.then((res) => res.data)
);
const [cityId, setCityId] = useState<number | undefined>(undefined) // Load cities when regionId exists
const { data: cities, isLoading: citiesLoading } = useSWR(
regionId ? `/general/cities?region_id=${regionId}` : null,
() =>
axios
.get(`/general/cities?region_id=${regionId}`, {
baseURL: import.meta.env.VITE_API_NEST_URL,
})
.then((res) => res.data)
);
const { data: boilers, isLoading: boilersLoading } = useSWR(cityId !== undefined ? `/fuel/boilers?city_id=${cityId}&offset=0&limit=100` : null, () => axios.get(`/fuel/boilers?city_id=${cityId}&offset=0&limit=100`, { // Load boilers when cityId exists
baseURL: import.meta.env.VITE_API_NEST_URL const { data: boilers, isLoading: boilersLoading } = useSWR(
}).then(res => res.data)) cityId ? `/fuel/boilers?city_id=${cityId}&offset=0&limit=100` : null,
() =>
axios
.get(`/fuel/boilers?city_id=${cityId}&offset=0&limit=100`, {
baseURL: import.meta.env.VITE_API_NEST_URL,
})
.then((res) => res.data)
);
//
// Sync regionId → URL
//
useEffect(() => {
if (!regionId) return;
setSearchParams((prev) => {
const params = new URLSearchParams(prev);
params.set("region_id", regionId.toString());
params.delete("city_id"); // reset city when region changes
return params;
});
}, [regionId]);
//
// Sync cityId → URL
//
useEffect(() => {
if (!cityId) return;
setSearchParams((prev) => {
const params = new URLSearchParams(prev);
params.set("city_id", cityId.toString());
return params;
});
}, [cityId]);
//
// Utility: get display values
//
const selectedRegion = regions?.find((r: IRegion) => r.id === regionId);
const selectedCity = cities?.find((c: ICity) => c.id === cityId);
useEffect(() => { useEffect(() => {
const handler = setTimeout(() => { const paramCityId = searchParams.get("city_id");
setDebouncedBoilerSearch(boilerSearch)
}, 500)
return () => { if (!paramCityId) return;
clearTimeout(handler)
const id = Number(paramCityId);
// If cityId not yet set OR mismatched with URL
if (cities && !cityId && cities.some((c: ICity) => c.id === id)) {
setCityId(id);
} }
}, [boilerSearch]) }, [cities])
useEffect(() => { useEffect(() => {
setBoilersPage(1) const paramRegionId = searchParams.get("region_id");
setBoilerSearch("") if (!paramRegionId) return;
}, [])
const id = Number(paramRegionId);
if (regions && !regionId && regions.some((r: IRegion) => r.id === id)) {
setRegionId(id);
}
}, [regions]);
useEffect(() => {
setCityId(undefined); // clear stale value before SWR runs
}, [regionId]);
return ( return (
<div style={{ <div style={{
@ -58,42 +126,51 @@ function Boilers() {
</Portal> */} </Portal> */}
<div style={{ display: 'flex', gap: '1rem' }}> <div style={{ display: 'flex', gap: '1rem' }}>
<Combobox {regionsLoading ?
placeholder="Выберите регион" <Spinner />
clearable :
<Dropdown
placeholder='Выберите район'
value={selectedRegion?.name ?? ""}
selectedOptions={regionId ? [regionId.toString()] : []}
onOptionSelect={(_, data) => { onOptionSelect={(_, data) => {
if (data.optionValue) { if (!data.optionValue) {
setRegionId(Number(data.optionValue)) setRegionId(undefined);
} else { return;
setCityId(undefined)
setRegionId(undefined)
} }
setRegionId(Number(data.optionValue));
}} }}
> >
{regions && Array.isArray(regions) && regions.map((option) => ( {regions && Array.isArray(regions) && regions.map((option) => (
<Option key={option.id} text={option.name} value={option.id}> <Option key={`region-${option.id}`} text={option.name} value={option.id}>
{option.name} {option.name}
</Option> </Option>
))} ))}
</Combobox> </Dropdown>
{cities && <Combobox
clearable
placeholder="Выберите населенный пункт"
onOptionSelect={(_, data) => {
if (data.optionValue) {
setCityId(Number(data.optionValue))
} else {
setCityId(undefined)
} }
{citiesLoading ?
<Spinner />
:
<Dropdown
placeholder='Выберите населенный пункт'
value={selectedCity?.name ?? ""}
selectedOptions={cityId ? [cityId.toString()] : []}
onOptionSelect={(_, data) => {
if (!data.optionValue) {
setCityId(undefined);
return;
}
setCityId(Number(data.optionValue));
}} }}
> >
{cities && Array.isArray(cities) && cities.map((option) => ( {cities && Array.isArray(cities) && cities.map((option) => (
<Option key={option.id} text={option.name} value={option.id}> <Option key={`region-${option.id}`} text={option.name} value={option.id}>
{option.name} {option.name}
</Option> </Option>
))} ))}
</Combobox>} </Dropdown>
}
</div> </div>
<div style={{ <div style={{
@ -102,10 +179,6 @@ function Boilers() {
flexDirection: 'column', flexDirection: 'column',
gap: '1rem' gap: '1rem'
}}> }}>
<Text size={400} weight='medium'>
{cityId && cities && Array.isArray(cities) && cities.find(city => city.id === cityId).name}
</Text>
{cityId && {cityId &&
<div style={{ display: 'flex', width: '100%', gap: '1rem', justifyContent: 'space-between' }}> <div style={{ display: 'flex', width: '100%', gap: '1rem', justifyContent: 'space-between' }}>
<BoilersCard title='Всего объектов' value={boilers && Array.isArray(boilers) ? boilers.length.toString() : ''} subtitle='' /> <BoilersCard title='Всего объектов' value={boilers && Array.isArray(boilers) ? boilers.length.toString() : ''} subtitle='' />
@ -119,11 +192,10 @@ function Boilers() {
} }
</div> </div>
{boilersLoading ?
<Spinner />
:
<AgGridReact <AgGridReact
// rowData={[
// Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test1' }), {}),
// Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test' }), {})
// ]}
key={`boilers-${cityId}`} key={`boilers-${cityId}`}
loading={boilersLoading} loading={boilersLoading}
overlayLoadingTemplate='Загрузка...' overlayLoadingTemplate='Загрузка...'
@ -159,40 +231,9 @@ function Boilers() {
defaultColDef={{ defaultColDef={{
flex: 1, flex: 1,
}} }}
/> />}
</div> </div>
) )
} }
const BoilersCard = ({
title,
value,
subtitle,
}: {
title: string
value: string
subtitle: string
}) => {
return (
<CompoundButton
onClick={() => { }}
style={{ display: 'flex', width: '100%', justifyContent: 'flex-start', flexDirection: 'column', gap: '2rem', alignItems: 'flex-start', cursor: 'pointer', userSelect: 'none' }}
//icon={icon}
>
<Text weight='bold' size={300}>
{title}
</Text>
<Text weight='bold' size={500}>
{value}
</Text>
<Text weight='regular' size={200} style={{ color: 'gray' }}>
{subtitle}
</Text>
</CompoundButton>
)
}
export default Boilers export default Boilers

View File

@ -0,0 +1,34 @@
import { CompoundButton, Text } from "@fluentui/react-components"
const BoilersCard = ({
title,
value,
subtitle,
}: {
title: string
value: string
subtitle: string
}) => {
return (
<CompoundButton
onClick={() => { }}
style={{ display: 'flex', width: '100%', justifyContent: 'flex-start', flexDirection: 'column', gap: '2rem', alignItems: 'flex-start', cursor: 'pointer', userSelect: 'none' }}
//icon={icon}
>
<Text weight='bold' size={300}>
{title}
</Text>
<Text weight='bold' size={500}>
{value}
</Text>
<Text weight='regular' size={200} style={{ color: 'gray' }}>
{subtitle}
</Text>
</CompoundButton>
)
}
export default BoilersCard

View File

@ -16,7 +16,7 @@ export default (params: CustomCellRendererProps) => {
<Table style={{ width: '100%' }} size='small'> <Table style={{ width: '100%' }} size='small'>
<TableBody> <TableBody>
{Array.isArray(params.value) && params.value.map(fuel => ( {Array.isArray(params.value) && params.value.map(fuel => (
<TableRow> <TableRow key={`${fuel.id}`}>
<TableCell> <TableCell>
<TableCellLayout truncate> <TableCellLayout truncate>
{fuel.name} {fuel.name}

View File

@ -97,7 +97,7 @@ const FuelsPage = () => {
}} }}
> >
{fuelTypes && Array.isArray(fuelTypes) && fuelTypes.map((option) => ( {fuelTypes && Array.isArray(fuelTypes) && fuelTypes.map((option) => (
<Option key={option.id} text={option.name} value={option.id}> <Option key={`fuel-type-${option.id}`} text={option.name} value={option.id}>
{option.name} {option.name}
</Option> </Option>
))} ))}
@ -131,7 +131,7 @@ const FuelsPage = () => {
setSelectedIdFuels(Number(data.value)) setSelectedIdFuels(Number(data.value))
}}> }}>
{fuelTypes && Array.isArray(fuelTypes) && fuelTypes.map((ft: FuelType) => ( {fuelTypes && Array.isArray(fuelTypes) && fuelTypes.map((ft: FuelType) => (
<Tab id={ft.id.toString()} value={ft.id}> <Tab id={ft.id.toString()} value={ft.id} key={ft.id}>
{ft.name} {ft.name}
</Tab> </Tab>
)) ))

View File

@ -1,6 +1,390 @@
const LimitsPage = () => { import { useEffect, useState } from 'react'
import { Button, CompoundButton, Dropdown, Option, Spinner, Text } from '@fluentui/react-components'
import useSWR from 'swr'
import axios from 'axios'
import { AgGridReact } from 'ag-grid-react'
import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community'
import { ICity, IRegion, ISeason } from '../../interfaces/fuel'
import { useSearchParams } from 'react-router-dom'
import LimitAddForm from './Limits/LimitEditForm'
ModuleRegistry.registerModules([AllCommunityModule])
function LimitsPage() {
const [searchParams, setSearchParams] = useSearchParams();
const [year, setYear] = useState<number | undefined>(
searchParams.get("year") ? Number(searchParams.get("year")) : undefined
)
const [regionId, setRegionId] = useState<number | undefined>(
searchParams.get("region_id") ? Number(searchParams.get("region_id")) : undefined
);
const [cityId, setCityId] = useState<number | undefined>(
searchParams.get("city_id") ? Number(searchParams.get("city_id")) : undefined
)
// Load regions
const { data: regions, isLoading: regionsLoading } = useSWR("/general/regions", () =>
axios
.get("/general/regions", { baseURL: import.meta.env.VITE_API_NEST_URL })
.then((res) => res.data)
);
// Load cities when regionId exists
const { data: cities, isLoading: citiesLoading } = useSWR(
regionId ? `/general/cities?region_id=${regionId}` : null,
() =>
axios
.get(`/general/cities?region_id=${regionId}`, {
baseURL: import.meta.env.VITE_API_NEST_URL,
})
.then((res) => res.data)
);
// Load seasons
const { data: seasons } = useSWR(
`/fuel/seasons`,
() => [
{
name: 'Отопительный сезон 2025-2026',
year: 2025
}
] as ISeason[]
);
// Load boilers when cityId exists
const { data: limits, isLoading: limitsLoading } = useSWR(
cityId && year ? `/fuel/limits?city_id=${cityId}&year=${year}&offset=0&limit=100` : null,
() =>
axios
.get(`/fuel/limits?city_id=${cityId}&year=${year}&offset=0&limit=100`, {
baseURL: import.meta.env.VITE_API_NEST_URL,
})
.then((res) => res.data)
);
//
// Sync regionId → URL
//
useEffect(() => {
if (!regionId) return;
setSearchParams((prev) => {
const params = new URLSearchParams(prev);
params.set("region_id", regionId.toString());
params.delete("city_id"); // reset city when region changes
return params;
});
}, [regionId]);
//
// Sync cityId → URL
//
useEffect(() => {
if (!cityId) return;
setSearchParams((prev) => {
const params = new URLSearchParams(prev);
params.set("city_id", cityId.toString());
return params;
});
}, [cityId]);
useEffect(() => {
if (!year) return;
setSearchParams((prev) => {
const params = new URLSearchParams(prev);
params.set("year", year.toString());
return params;
});
}, [year]);
//
// Utility: get display values
//
const selectedRegion = regions?.find((r: IRegion) => r.id === regionId);
const selectedCity = cities?.find((c: ICity) => c.id === cityId);
const selectedSeason = seasons?.find((c: ISeason) => c.year === year);
useEffect(() => {
const paramCityId = searchParams.get("city_id");
if (!paramCityId) return;
const id = Number(paramCityId);
// If cityId not yet set OR mismatched with URL
if (cities && !cityId && cities.some((c: ICity) => c.id === id)) {
setCityId(id);
}
}, [cities])
useEffect(() => {
const paramRegionId = searchParams.get("region_id");
if (!paramRegionId) return;
const id = Number(paramRegionId);
if (regions && !regionId && regions.some((r: IRegion) => r.id === id)) {
setRegionId(id);
}
}, [regions]);
useEffect(() => {
const paramYear = searchParams.get("year");
if (!paramYear) return;
const year = Number(paramYear);
if (year && seasons) {
setYear(year);
}
}, [seasons]);
useEffect(() => {
setCityId(undefined); // clear stale value before SWR runs
setYear(undefined)
}, [regionId])
const [limitAddFormOpen, setLimitAddFormOpen] = useState(false)
return ( return (
<div>LimitsPage</div> <div style={{
display: 'flex',
flexDirection: 'column',
padding: '1rem',
width: '100%',
gap: '1rem'
}}>
{/* <Portal mountNode={document.querySelector('#header-portal')}>
</Portal> */}
<div style={{ display: 'flex', gap: '1rem' }}>
{regionsLoading ?
<Spinner />
:
<Dropdown
placeholder='Выберите район'
value={selectedRegion?.name ?? ""}
selectedOptions={regionId ? [regionId.toString()] : []}
onOptionSelect={(_, data) => {
if (!data.optionValue) {
setRegionId(undefined);
return;
}
setRegionId(Number(data.optionValue));
}}
>
{regions && Array.isArray(regions) && regions.map((option) => (
<Option key={`region-${option.id}`} text={option.name} value={option.id}>
{option.name}
</Option>
))}
</Dropdown>
}
{citiesLoading ?
<Spinner />
:
<Dropdown
placeholder='Выберите населенный пункт'
value={selectedCity?.name ?? ""}
selectedOptions={cityId ? [cityId.toString()] : []}
onOptionSelect={(_, data) => {
if (!data.optionValue) {
setCityId(undefined);
return;
}
setCityId(Number(data.optionValue));
}}
>
{cities && Array.isArray(cities) && cities.map((option) => (
<Option key={`region-${option.id}`} text={option.name} value={option.id}>
{option.name}
</Option>
))}
</Dropdown>
}
<Dropdown
placeholder='Выберите отопительный сезон'
value={selectedSeason?.name ?? ""}
selectedOptions={year ? [year.toString()] : []}
onOptionSelect={(_, data) => {
if (!data.optionValue) {
setYear(undefined);
return;
}
setYear(Number(data.optionValue));
}}
>
{seasons && Array.isArray(seasons) && seasons.map((option) => (
<Option key={`region-${option.year}`} text={option.name} value={option.year.toString()}>
{option.name}
</Option>
))}
</Dropdown>
</div>
<div style={{
display: 'flex',
width: '100%',
flexDirection: 'column',
gap: '1rem'
}}>
<Text size={400} weight='medium'>
{cityId && cities && Array.isArray(cities) && cities.find(city => city.id === cityId).name}
</Text>
{cityId &&
<div style={{ display: 'flex', width: '100%', gap: '1rem', justifyContent: 'space-between' }}>
<BoilersCard title='Всего объектов' value={limits && Array.isArray(limits) ? limits.length.toString() : ''} subtitle='' />
<BoilersCard title='Общий остаток' value={''} subtitle='' />
<BoilersCard title='Лимит на сезон' value={''} subtitle='' />
<BoilersCard title='Требуют внимания' value={''} subtitle='' />
</div>
}
<Button appearance='primary' style={{ width: 'min-content' }} onClick={() => setLimitAddFormOpen(true)}>
Добавить
</Button>
<LimitAddForm cityId={cityId} open={limitAddFormOpen} setOpen={setLimitAddFormOpen} />
</div>
<AgGridReact
key={`boilers-${cityId}`}
loading={limitsLoading}
overlayLoadingTemplate='Загрузка...'
overlayNoRowsTemplate='Нет данных'
rowData={limits}
onRowClicked={(e) => {
console.log(e.data)
}
}
columnDefs={[
{
field: 'boiler_name',
headerName: 'Наименование'
},
{
field: 'fuel_name',
headerName: 'Вид топлива'
},
// {
// field: 'boiler_code',
// headerName: 'Идент. код'
// },
// {
// field: 'id_fuels',
// headerName: 'Вид топлива',
// //editable: true,
// //cellEditor: FuelTypeEditor,
// autoHeight: true,
// cellRenderer: FuelRenderer,
// cellStyle: (_) => {
// return { padding: '1px' }
// }
// //enableCellChangeFlash: true
// },
// {
// field: 'activity',
// headerName: 'Активный',
// cellRenderer: (params: CustomCellRendererProps) => (<span>{params.value === true ? 'Да' : 'Нет'}</span>)
// }
{
field: 'jul',
headerName: 'Июль'
},
{
field: 'aug',
headerName: 'Август'
},
{
field: 'sep',
headerName: 'Сентябрь'
},
{
field: 'oct',
headerName: 'Октябрь'
},
{
field: 'nov',
headerName: 'Ноябрь'
},
{
field: 'dec',
headerName: 'Декабрь'
},
{
field: 'jan',
headerName: 'Январь'
},
{
field: 'feb',
headerName: 'Февраль'
},
{
field: 'mar',
headerName: 'Март'
},
{
field: 'apr',
headerName: 'Апрель'
},
{
field: 'may',
headerName: 'Май'
},
{
field: 'jun',
headerName: 'Июнь'
}
]}
defaultColDef={{
flex: 1,
}}
/>
</div>
)
}
const BoilersCard = ({
title,
value,
subtitle,
}: {
title: string
value: string
subtitle: string
}) => {
return (
<CompoundButton
onClick={() => { }}
style={{ display: 'flex', width: '100%', justifyContent: 'flex-start', flexDirection: 'column', gap: '2rem', alignItems: 'flex-start', cursor: 'pointer', userSelect: 'none' }}
//icon={icon}
>
<Text weight='bold' size={300}>
{title}
</Text>
<Text weight='bold' size={500}>
{value}
</Text>
<Text weight='regular' size={200} style={{ color: 'gray' }}>
{subtitle}
</Text>
</CompoundButton>
) )
} }

View File

@ -0,0 +1,287 @@
import { Button, Dialog, DialogBody, DialogContent, DialogSurface, DialogTitle, Field, Input, ProgressBar, Spinner } from '@fluentui/react-components'
import { DataPieColor } from '@fluentui/react-icons'
import axios from 'axios'
import { useEffect, useState } from 'react'
import { Controller, SubmitHandler, useFieldArray, useForm, useWatch } from 'react-hook-form'
import useSWR from 'swr'
type Month = {
month: number
value: number
}
type Inputs = {
id_boiler: string
id_fuel: string
year: number
months: Month[]
}
const months = [
{
id: 7,
month: 'jul',
label: 'Июль'
},
{
id: 8,
month: 'aug',
label: 'Август'
},
{
id: 9,
month: 'sep',
label: 'Сентябрь'
},
{
id: 10,
month: 'nov',
label: 'Октябрь'
},
{
id: 11,
month: 'oct',
label: 'Ноябрь'
},
{
id: 12,
month: 'dec',
label: 'Декабрь'
},
{
id: 1,
month: 'jan',
label: 'Январь'
},
{
id: 2,
month: 'feb',
label: 'Февраль'
},
{
id: 3,
month: 'mar',
label: 'Март'
},
{
id: 4,
month: 'apr',
label: 'Апрель'
},
{
id: 5,
month: 'may',
label: 'Май'
},
{
id: 6,
month: 'jun',
label: 'Июнь'
},
]
const LimitAddForm = ({
cityId,
open,
setOpen,
percentage
}: {
cityId: number | undefined
open: boolean
setOpen: (open: boolean) => void
percentage?: boolean
}) => {
const {
handleSubmit,
control,
setValue,
formState: { isSubmitting },
} = useForm<Inputs>({
defaultValues: {
months: Array.from({ length: 12 }, (_, i) => ({
month: i + 1,
value: 0,
}))
}
})
const { fields } = useFieldArray({
control,
name: 'months'
})
const onSubmit: SubmitHandler<Inputs> = async (data) => {
await axios.post(`/fuel/limits`, {
id_boiler: '06407B1C-C23F-44C8-BADF-4653060EB784',
id_fuel: 3,
year: 2025,
months: data.months
}, {
baseURL: import.meta.env.VITE_API_NEST_URL,
})
//mutateFuels([...fuels, data])
// setTimeout(() => {
// console.log("done")
// }, 1000)
await new Promise((resolve) => {
setTimeout(() => resolve(console.log("done")), 1000);
})
}
const [overallLimit, setOverallLimit] = useState(0)
const watchedMonths = useWatch({
control,
name: "months",
})
const { data: citySettings } = useSWR(
cityId ? `/fuel/city-settings?city_id=${cityId}` : null,
() =>
axios
.get(`/fuel/city-settings?city_id=${cityId}`, {
baseURL: import.meta.env.VITE_API_NEST_URL,
})
.then((res) => res.data)
)
const handlePartition = () => {
if (citySettings && Array.isArray(citySettings) && citySettings.length > 0) {
citySettings.map(s => {
setValue(`months.${Number(s.month - 1)}.value`, Number((Number((overallLimit / 100).toFixed(3)) * s.procent).toFixed(3)))
})
}
}
useEffect(() => {
if (watchedMonths) {
let sum = 0
watchedMonths.map(wm => sum = sum + wm.value)
setOverallLimit(Number(sum.toFixed(3)))
}
}, [watchedMonths])
return (
<Dialog open={open} onOpenChange={(_, data) => setOpen(data.open)}>
<DialogSurface>
<DialogBody>
{isSubmitting &&
<div style={{ position: 'absolute', inset: 0, zIndex: '1', background: '#00000030', display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}>
<Spinner />
</div>
}
<DialogTitle>Распределение лимитов</DialogTitle>
<DialogContent
style={{
display: 'flex', flexDirection: 'column', gap: '1rem',
minWidth: 'fit-content', minHeight: 'fit-content'
}}
>
<form onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
handlePartition()
}} style={{ display: 'flex', width: '100%', gap: '0.25rem', justifyContent: 'flex-end' }}>
<Field style={{ display: 'flex' }} label='Лимит расхода котельного топлива за сезон' orientation='horizontal'>
<Input type='number' value={overallLimit.toString()} onChange={(_, data) => setOverallLimit(Number(data.value))} />
</Field>
<Button title='Распределить' type='submit' icon={<DataPieColor />}></Button>
</form>
<form onSubmit={handleSubmit(onSubmit)}>
<div style={{ display: 'flex', flexDirection: 'row', gap: '1rem', width: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', width: '50%', gap: '0.25rem' }}>
{fields.map((f, index) => {
if (f.month >= 7 && f.month <= 12) {
return (
<Controller
key={f.month}
name={`months.${index}.value`}
control={control}
render={({ field }) => (
<Field style={{ gridTemplateColumns: '1fr auto' }} key={f.month} validationMessage={percentage ? `${(field.value / (overallLimit / 100)).toFixed(1)}%` : undefined} validationState='none' label={months.find(m => m.id === f.month)?.label} orientation='horizontal'>
<Input value={field.value.toString()} onChange={(_, data) => field.onChange(Number(data.value))} />
{percentage && <ProgressBar value={field.value / (overallLimit / 100) * 0.01} />}
</Field>
)}
/>
)
}
})}
<Field style={{ gridTemplateColumns: '1fr auto' }} validationMessage={percentage ? `${(watchedMonths.filter(month => month.month >= 7 && month.month <= 12)
.reduce((sum, month) => {
const value = Number(month.value) || 0;
return sum + value;
}, 0) / (overallLimit / 100)).toFixed(1)}%` : undefined} validationState='none' label={'2 полугодие'} orientation='horizontal'>
<Input disabled value={watchedMonths.filter(month => month.month >= 7 && month.month <= 12)
.reduce((sum, month) => {
const value = Number(month.value) || 0;
return sum + value;
}, 0).toFixed(3)} />
{percentage && <ProgressBar value={watchedMonths.filter(month => month.month >= 7 && month.month <= 12)
.reduce((sum, month) => {
const value = Number(month.value) || 0;
return sum + value;
}, 0) / (overallLimit / 100) * 0.01} />}
</Field>
</div>
<div style={{ display: 'flex', flexDirection: 'column', width: '50%', gap: '0.25rem' }}>
{fields.map((f, index) => {
if (f.month >= 1 && f.month <= 6) {
return (
<Controller
key={f.month}
name={`months.${index}.value`}
control={control}
render={({ field }) => (
<Field style={{ gridTemplateColumns: '1fr auto' }} key={f.month} validationMessage={percentage ? `${(field.value / (overallLimit / 100)).toFixed(1)}%` : undefined} validationState='none' label={months.find(m => m.id === f.month)?.label} orientation='horizontal'>
<Input value={field.value.toString()} onChange={(_, data) => field.onChange(Number(data.value))} />
{percentage && <ProgressBar value={field.value / (overallLimit / 100) * 0.01} />}
</Field>
)}
/>
)
}
})}
<Field style={{ gridTemplateColumns: '1fr auto' }} validationMessage={percentage ? `${(watchedMonths.filter(month => month.month >= 1 && month.month <= 6)
.reduce((sum, month) => {
const value = Number(month.value) || 0;
return sum + value;
}, 0) / (overallLimit / 100)).toFixed(1)}%` : undefined} validationState='none' label={'1 полугодие'} orientation='horizontal'>
<Input disabled value={watchedMonths.filter(month => month.month >= 1 && month.month <= 6)
.reduce((sum, month) => {
const value = Number(month.value) || 0;
return sum + value;
}, 0).toFixed(3)} />
{percentage && <ProgressBar value={watchedMonths.filter(month => month.month >= 1 && month.month <= 6)
.reduce((sum, month) => {
const value = Number(month.value) || 0;
return sum + value;
}, 0) / (overallLimit / 100) * 0.01} />}
</Field>
</div>
</div>
<div style={{ display: 'flex', marginTop: '1rem', justifyContent: 'flex-end' }}>
<Button type='submit' appearance='primary'>
Добавить
</Button>
</div>
</form>
</DialogContent>
</DialogBody>
</DialogSurface>
</Dialog>
)
}
export default LimitAddForm

19
client/src/store/fuel.ts Normal file
View File

@ -0,0 +1,19 @@
import { create } from 'zustand'
export interface FuelState {
regionId: number | undefined
cityId: number | undefined
}
export const useFuelStore = create<FuelState>(() => {
return {
regionId: undefined,
cityId: undefined
}
})
export const getRegionId = () => useFuelStore.getState().regionId
export const setRegionId = (id: number | undefined) => useFuelStore.setState(() => ({ regionId: id }))
export const getCityId = () => useFuelStore.getState().cityId
export const setCityId = (id: number | undefined) => useFuelStore.setState(() => ({ cityId: id }))

View File

@ -1,5 +1,14 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from "@nestjs/swagger";
import { IsNumber, IsUUID } from "class-validator"; import { Type } from "class-transformer";
import { IsArray, IsNumber, IsObject, IsUUID, ValidateNested } from "class-validator";
class MonthDto {
@IsNumber()
month: number
@IsNumber()
value: number
}
export class CreateLimitDto { export class CreateLimitDto {
@ApiProperty({ format: 'uuid' }) @ApiProperty({ format: 'uuid' })
@ -10,15 +19,13 @@ export class CreateLimitDto {
@IsNumber() @IsNumber()
id_fuel: number id_fuel: number
@ApiProperty()
@IsNumber()
month: Date
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
year: number year: number
@ApiProperty() @ApiProperty()
@IsNumber() @IsArray()
value: number @ValidateNested({ each: true })
@Type(() => MonthDto)
months: MonthDto[]
} }

View File

@ -0,0 +1,18 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"
import { IsNumberString, IsOptional } from "class-validator"
export class GetCitySettingsDTO {
@ApiProperty()
@IsNumberString()
city_id: number
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
offset?: number
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
limit?: number
}

View File

@ -0,0 +1,22 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"
import { IsNumberString, IsOptional } from "class-validator"
export class GetLimitsDTO {
@ApiProperty()
@IsNumberString()
city_id: number
@ApiProperty()
@IsNumberString()
year: number
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
offset?: number
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
limit?: number
}

View File

@ -9,6 +9,8 @@ import { FuelExpenseDto } from './dto/expense'
import { FuelTransferDto } from './dto/transfer' import { FuelTransferDto } from './dto/transfer'
import { GetBoilersDTO } from './dto/get-boilers' import { GetBoilersDTO } from './dto/get-boilers'
import { GetFuelsDTO } from './dto/get-fuels' import { GetFuelsDTO } from './dto/get-fuels'
import { GetLimitsDTO } from './dto/get-limits'
import { GetCitySettingsDTO } from './dto/get-city-settings'
@Controller('fuel') @Controller('fuel')
export class FuelController { export class FuelController {
@ -43,8 +45,8 @@ export class FuelController {
type: FuelLimitDto, type: FuelLimitDto,
isArray: true, isArray: true,
}) })
async getBoilersFuelsLimits(): Promise<FuelLimitDto[]> { async getBoilersFuelsLimits(@Query() getLimitsDTO: GetLimitsDTO): Promise<FuelLimitDto[]> {
return this.fuelService.getBoilersFuelsLimits() return this.fuelService.getBoilersFuelsLimits(getLimitsDTO)
} }
@Post('/limits') @Post('/limits')
@ -52,6 +54,16 @@ export class FuelController {
return this.fuelService.addBoilersFuelsLimit(createLimitDto) return this.fuelService.addBoilersFuelsLimit(createLimitDto)
} }
// Fuel limits
@Get('/city-settings')
@ApiOkResponse({
description: 'City settings',
isArray: true,
})
async getCitySettings(@Query() getCitySettingsDTO: GetCitySettingsDTO): Promise<any[]> {
return this.fuelService.getCitySettings(getCitySettingsDTO)
}
// Fuel expenses // Fuel expenses
@Get('/expenses') @Get('/expenses')
@ApiOkResponse({ @ApiOkResponse({

View File

@ -1,10 +1,12 @@
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm'; import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { CreateExpenseDto } from './dto/create-expense'; import { CreateExpenseDto } from './dto/create-expense';
import { CreateLimitDto } from './dto/create-limit'; import { CreateLimitDto } from './dto/create-limit';
import { CreateTransferDto } from './dto/create-transfer'; import { CreateTransferDto } from './dto/create-transfer';
import { GetFuelsDTO } from './dto/get-fuels'; import { GetFuelsDTO } from './dto/get-fuels';
import { GetLimitsDTO } from './dto/get-limits';
import { GetCitySettingsDTO } from './dto/get-city-settings';
@Injectable() @Injectable()
export class FuelService { export class FuelService {
@ -56,13 +58,12 @@ export class FuelService {
const result = await this.wsDataSource.query(` const result = await this.wsDataSource.query(`
SELECT SELECT
b.*, b.*,
COALESCE( (
(SELECT fp.* SELECT fp.*
FROM isFuels.dbo.BoilersFuels bf FROM isFuels.dbo.BoilersFuels bf
INNER JOIN isWorldstone.dbo.dFuelsParameters fp ON bf.id_fuels = fp.id INNER JOIN isWorldstone.dbo.dFuelsParameters fp ON bf.id_fuels = fp.id
WHERE bf.id_boilers = b.id_object WHERE bf.id_boilers = b.id_object
FOR JSON PATH), FOR JSON PATH
'[]'
) AS id_fuels ) AS id_fuels
FROM isWorldstone.dbo.vBoilers b FROM isWorldstone.dbo.vBoilers b
WHERE id_city = @0 WHERE id_city = @0
@ -90,11 +91,118 @@ export class FuelService {
return result return result
} }
async getBoilersFuelsLimits(): Promise<any[]> { async getCitySettings(getCitySettings: GetCitySettingsDTO): Promise<any[]> {
const { city_id } = getCitySettings
const result = await this.wsDataSource.query(` const result = await this.wsDataSource.query(`
SELECT * FROM isWorldstone..vCitySettings
WHERE id_city = @0
`, [Number(city_id)])
return result
}
async getBoilersFuelsLimits(getLimitsDTO: GetLimitsDTO): Promise<any[]> {
const { city_id, year, offset, limit } = getLimitsDTO
// const result = await this.wsDataSource.query(`
// SELECT
// b.*,
// (
// SELECT
// bf.id AS id_fuels_entry,
// df.id AS fuel_id,
// df.name,
// -- вложенный JSON: лимиты по месяцам
// (
// SELECT
// l.month,
// l.year,
// l.value
// FROM isFuels..BoilersFuelsLimits l
// WHERE l.id_fuel = bf.id
// ORDER BY l.year, l.month
// FOR JSON PATH
// ) AS limits
// FROM isFuels..BoilersFuels bf
// JOIN isWorldstone..dFuelsParameters df
// ON df.id = bf.id_fuels
// WHERE bf.id_boilers = b.id_object
// FOR JSON PATH
// ) AS fuels
// FROM isWorldstone..vBoilers b
// WHERE id_city = @0
// ORDER BY id_object
// OFFSET @1 ROWS
// FETCH NEXT @2 ROWS ONLY
// ;
// `, [city_id, Number(offset || 0), Number(limit || 100)])
const result = await this.wsDataSource.query(`
WITH Src AS (
SELECT
b.id_object AS id_boiler,
b.name AS boiler_name,
bf.id AS id_boiler_fuel,
bf.id_fuels AS id_fuel_type,
fp.name AS fuel_name,
l.month,
l.value,
l.year
FROM isWorldstone..vBoilers b
LEFT JOIN isFuels..BoilersFuels bf
ON bf.id_boilers = b.id_object
LEFT JOIN isWorldstone..dFuelsParameters fp
ON fp.id = bf.id_fuels
LEFT JOIN isFuels..BoilersFuelsLimits l
ON l.id_boiler = bf.id_boilers
AND l.id_fuel = bf.id
AND (
(l.month BETWEEN 7 AND 12 AND l.year = @1) -- second half
OR
(l.month BETWEEN 1 AND 6 AND l.year = @2) -- first half next year
)
WHERE b.id_city = @0
),
Renamed AS (
SELECT
id_boiler,
boiler_name,
id_boiler_fuel,
id_fuel_type,
fuel_name,
CASE month
WHEN 7 THEN 'jul'
WHEN 8 THEN 'aug'
WHEN 9 THEN 'sep'
WHEN 10 THEN 'oct'
WHEN 11 THEN 'nov'
WHEN 12 THEN 'dec'
WHEN 1 THEN 'jan'
WHEN 2 THEN 'feb'
WHEN 3 THEN 'mar'
WHEN 4 THEN 'apr'
WHEN 5 THEN 'may'
WHEN 6 THEN 'jun'
END AS MonthName,
value
FROM Src
)
SELECT * SELECT *
FROM "vBoilerLimits"; FROM Renamed
`) PIVOT (
MAX(value)
FOR MonthName IN (
[jul], [aug], [sep], [oct], [nov], [dec],
[jan], [feb], [mar], [apr], [may], [jun]
)
) AS P
ORDER BY id_boiler, id_boiler_fuel;
`, [city_id, year, year + 1])
return result return result
} }
@ -114,10 +222,14 @@ export class FuelService {
} }
async addBoilersFuelsLimit(createLimitDto: CreateLimitDto): Promise<any[]> { async addBoilersFuelsLimit(createLimitDto: CreateLimitDto): Promise<any[]> {
const result = await this.dataSource.query(` const logger = new Logger('INFO'); // 'MyContext' is an optional string for log context
INSERT INTO dbo.BoilersFuelsLimits (id_boiler, id_fuel, value, month, year) VALUES ($1, $2, $3, $4, $5) logger.log(createLimitDto)
`, [createLimitDto.id_boiler, createLimitDto.id_fuel, createLimitDto.value, createLimitDto.month, createLimitDto.year])
return result return [createLimitDto]
// const result = await this.dataSource.query(`
// INSERT INTO isFuels..BoilersFuelsLimits (id_boiler, id_fuel, value, month, year) VALUES ($1, $2, $3, $4, $5)
// `, [createLimitDto.id_boiler, createLimitDto.id_fuel, createLimitDto.value, createLimitDto.month, createLimitDto.year])
// return result
} }
async addFuelsTransfer(createTransferDto: CreateTransferDto): Promise<any[]> { async addFuelsTransfer(createTransferDto: CreateTransferDto): Promise<any[]> {