Compare commits
No commits in common. "658bfe65152014233d6ff7c85f48ef1b3b1b37bf" and "d551939e9958616a1576f7b87ace1cfbbfed2606" have entirely different histories.
658bfe6515
...
d551939e99
@ -78,7 +78,6 @@ async def get_projects_with_reports(
|
|||||||
limit: Optional[int] = None,
|
limit: Optional[int] = None,
|
||||||
offset: Optional[int] = None,
|
offset: Optional[int] = None,
|
||||||
status_in: Optional[str] = None,
|
status_in: Optional[str] = None,
|
||||||
search: Optional[str] = None,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
) -> BaseListResponse[dict]:
|
) -> BaseListResponse[dict]:
|
||||||
@ -92,7 +91,6 @@ async def get_projects_with_reports(
|
|||||||
limit=limit,
|
limit=limit,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
status_in=status_in,
|
status_in=status_in,
|
||||||
search=search,
|
|
||||||
)
|
)
|
||||||
return BaseListResponse(result=projects, count=count)
|
return BaseListResponse(result=projects, count=count)
|
||||||
|
|
||||||
@ -371,4 +369,4 @@ async def add_project(
|
|||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
"limit_report_id": limit_report_id,
|
"limit_report_id": limit_report_id,
|
||||||
"current_expenses_report_id": current_expenses_report_id,
|
"current_expenses_report_id": current_expenses_report_id,
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from sqlalchemy import String, cast, exists, false, func, or_, select, text
|
from sqlalchemy import exists, false, func, select, text
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.db.models.org_unit import OrgUnit
|
from src.db.models.org_unit import OrgUnit
|
||||||
@ -45,7 +45,6 @@ class ProjectRepository:
|
|||||||
branch_id: int | None,
|
branch_id: int | None,
|
||||||
org_unit_ids: list[int] | None,
|
org_unit_ids: list[int] | None,
|
||||||
status_in: list[str] | None = None,
|
status_in: list[str] | None = None,
|
||||||
search: str | None = None,
|
|
||||||
):
|
):
|
||||||
if year is not None:
|
if year is not None:
|
||||||
query = query.where(
|
query = query.where(
|
||||||
@ -63,27 +62,6 @@ class ProjectRepository:
|
|||||||
query = query.where(Project.org_unit_id == branch_id)
|
query = query.where(Project.org_unit_id == branch_id)
|
||||||
if status_in is not None:
|
if status_in is not None:
|
||||||
query = query.where(Project.status.in_(status_in))
|
query = query.where(Project.status.in_(status_in))
|
||||||
if search is not None and (normalized_search := search.strip()):
|
|
||||||
search_pattern = (
|
|
||||||
f"%{normalized_search.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')}%"
|
|
||||||
)
|
|
||||||
matching_year_exists = exists(
|
|
||||||
select(1)
|
|
||||||
.select_from(RfProjectYear)
|
|
||||||
.where(
|
|
||||||
RfProjectYear.project_id == Project.id,
|
|
||||||
func.concat("Смета_", cast(RfProjectYear.year, String)).ilike(
|
|
||||||
search_pattern,
|
|
||||||
escape="\\",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
query = query.where(
|
|
||||||
or_(
|
|
||||||
Project.name.ilike(search_pattern, escape="\\"),
|
|
||||||
matching_year_exists,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if org_unit_ids is not None:
|
if org_unit_ids is not None:
|
||||||
if len(org_unit_ids) == 0:
|
if len(org_unit_ids) == 0:
|
||||||
query = query.where(false())
|
query = query.where(false())
|
||||||
@ -193,11 +171,10 @@ class ProjectRepository:
|
|||||||
with_count: bool = False,
|
with_count: bool = False,
|
||||||
org_unit_ids: list[int] | None = None,
|
org_unit_ids: list[int] | None = None,
|
||||||
status_in: list[str] | None = None,
|
status_in: list[str] | None = None,
|
||||||
search: str | None = None,
|
|
||||||
) -> list[dict] | tuple[int, list[dict]]:
|
) -> list[dict] | tuple[int, list[dict]]:
|
||||||
project_query = select(Project).order_by(Project.id)
|
project_query = select(Project).order_by(Project.id)
|
||||||
project_query = self._apply_project_filters(
|
project_query = self._apply_project_filters(
|
||||||
project_query, year, branch_id, org_unit_ids, status_in, search
|
project_query, year, branch_id, org_unit_ids, status_in
|
||||||
)
|
)
|
||||||
if offset is not None:
|
if offset is not None:
|
||||||
project_query = project_query.offset(offset)
|
project_query = project_query.offset(offset)
|
||||||
@ -216,8 +193,6 @@ class ProjectRepository:
|
|||||||
.order_by(RfProjectYear.year)
|
.order_by(RfProjectYear.year)
|
||||||
)
|
)
|
||||||
for rpy, smeta in (await self.db.execute(years_query)).all():
|
for rpy, smeta in (await self.db.execute(years_query)).all():
|
||||||
if search and search.strip().lower() not in f"Смета_{rpy.year}".lower():
|
|
||||||
continue
|
|
||||||
years_by_project.setdefault(rpy.project_id, []).append((rpy, smeta))
|
years_by_project.setdefault(rpy.project_id, []).append((rpy, smeta))
|
||||||
|
|
||||||
payload = [
|
payload = [
|
||||||
@ -230,7 +205,7 @@ class ProjectRepository:
|
|||||||
|
|
||||||
count_query = select(func.count(Project.id))
|
count_query = select(func.count(Project.id))
|
||||||
count_query = self._apply_project_filters(
|
count_query = self._apply_project_filters(
|
||||||
count_query, year, branch_id, org_unit_ids, status_in, search
|
count_query, year, branch_id, org_unit_ids, status_in
|
||||||
)
|
)
|
||||||
total = int((await self.db.execute(count_query)).scalar() or 0)
|
total = int((await self.db.execute(count_query)).scalar() or 0)
|
||||||
return total, payload
|
return total, payload
|
||||||
|
|||||||
@ -44,7 +44,6 @@ class ProjectService:
|
|||||||
limit: int | None = None,
|
limit: int | None = None,
|
||||||
with_count: bool = False,
|
with_count: bool = False,
|
||||||
status_in: list[str] | None = None,
|
status_in: list[str] | None = None,
|
||||||
search: str | None = None,
|
|
||||||
) -> list[dict] | tuple[int, list[dict]]:
|
) -> list[dict] | tuple[int, list[dict]]:
|
||||||
org_unit_ids = await self._allowed_org_unit_ids(user)
|
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||||
return await self.project_repo.get_with_reports(
|
return await self.project_repo.get_with_reports(
|
||||||
@ -55,7 +54,6 @@ class ProjectService:
|
|||||||
with_count=with_count,
|
with_count=with_count,
|
||||||
org_unit_ids=org_unit_ids,
|
org_unit_ids=org_unit_ids,
|
||||||
status_in=status_in,
|
status_in=status_in,
|
||||||
search=search,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get(self, project_id: int, user: AppUser) -> dict | None:
|
async def get(self, project_id: int, user: AppUser) -> dict | None:
|
||||||
|
|||||||
@ -327,33 +327,6 @@ def test_projects_with_reports_filters_by_multiple_statuses(client, admin_tokens
|
|||||||
assert payload["count"] == len(payload["result"])
|
assert payload["count"] == len(payload["result"])
|
||||||
|
|
||||||
|
|
||||||
def test_projects_with_reports_applies_search_before_pagination(client, admin_tokens, auth_headers):
|
|
||||||
project_id, project_name = _create_project(client, admin_tokens, auth_headers)
|
|
||||||
update_response = client.patch(
|
|
||||||
f"/api/v1/project/{project_id}",
|
|
||||||
json={"name": project_name, "status": "archived"},
|
|
||||||
headers=auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert update_response.status_code == 200
|
|
||||||
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/projects/with-reports",
|
|
||||||
params={
|
|
||||||
"status_in": "archived",
|
|
||||||
"search": project_name.lower(),
|
|
||||||
"offset": 0,
|
|
||||||
"limit": 1,
|
|
||||||
},
|
|
||||||
headers=auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert payload["count"] == 1
|
|
||||||
assert [project["id"] for project in payload["result"]] == [project_id]
|
|
||||||
assert payload["result"][0]["sub_rows"] == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_upd_project_smoke(client, admin_tokens, auth_headers):
|
def test_upd_project_smoke(client, admin_tokens, auth_headers):
|
||||||
project_id, name = _create_project(client, admin_tokens, auth_headers)
|
project_id, name = _create_project(client, admin_tokens, auth_headers)
|
||||||
|
|
||||||
|
|||||||
@ -227,37 +227,37 @@ export const Eye = styled.div`
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const HiddenExpenseItems = ({ size = 20, color = '#4A5565' }) => (
|
export const HiddenExpenseItems = ({ size = 20 }) => (
|
||||||
<svg width={size} height={size} viewBox='0 0 20 20' fill='none' aria-hidden='true'>
|
<svg width={size} height={size} viewBox='0 0 20 20' fill='none' aria-hidden='true'>
|
||||||
<path d='M3.25 5.25h9.5M3.25 9.25h7.5M3.25 13.25h5.5' stroke={color} strokeWidth='1.25' strokeLinecap='round' />
|
<path d='M3.25 5.25h9.5M3.25 9.25h7.5M3.25 13.25h5.5' stroke='currentColor' strokeWidth='1.25' strokeLinecap='round' />
|
||||||
<path d='M11.2 12.15c.83.72 1.77 1.1 2.8 1.1 1.73 0 3.04-1.07 3.75-2-.37-.48-.87-.98-1.5-1.37' stroke={color} strokeWidth='1.25' strokeLinecap='round' strokeLinejoin='round' />
|
<path d='M11.2 12.15c.83.72 1.77 1.1 2.8 1.1 1.73 0 3.04-1.07 3.75-2-.37-.48-.87-.98-1.5-1.37' stroke='currentColor' strokeWidth='1.25' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
<path d='M10.25 8.9A5.8 5.8 0 0 1 14 7.25c.68 0 1.31.17 1.88.45M10 5l7 10' stroke={color} strokeWidth='1.25' strokeLinecap='round' strokeLinejoin='round' />
|
<path d='M10.25 8.9A5.8 5.8 0 0 1 14 7.25c.68 0 1.31.17 1.88.45M10 5l7 10' stroke='currentColor' strokeWidth='1.25' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const VisibleExpenseItems = ({ size = 20, color = '#258141' }) => (
|
export const VisibleExpenseItems = ({ size = 20 }) => (
|
||||||
<svg width={size} height={size} viewBox='0 0 20 20' fill='none' aria-hidden='true'>
|
<svg width={size} height={size} viewBox='0 0 20 20' fill='none' aria-hidden='true'>
|
||||||
<path d='M3.25 5.25h9.5M3.25 9.25h7.5M3.25 13.25h5.5' stroke={color} strokeWidth='1.25' strokeLinecap='round' />
|
<path d='M3.25 5.25h9.5M3.25 9.25h7.5M3.25 13.25h5.5' stroke='currentColor' strokeWidth='1.25' strokeLinecap='round' />
|
||||||
<path d='M10.25 11.25c.77-1 2.03-2 3.75-2s2.98 1 3.75 2c-.77 1-2.03 2-3.75 2s-2.98-1-3.75-2Z' stroke={color} strokeWidth='1.25' strokeLinecap='round' strokeLinejoin='round' />
|
<path d='M10.25 11.25c.77-1 2.03-2 3.75-2s2.98 1 3.75 2c-.77 1-2.03 2-3.75 2s-2.98-1-3.75-2Z' stroke='currentColor' strokeWidth='1.25' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
<circle cx='14' cy='11.25' r='1' fill={color} />
|
<circle cx='14' cy='11.25' r='1' fill='currentColor' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CurrentStageColumns = ({ size = 20, color = '#258141', mutedColor = '#99A1AF' }) => (
|
export const CurrentStageColumns = ({ size = 20 }) => (
|
||||||
<svg width={size} height={size} viewBox='0 0 20 20' fill='none' aria-hidden='true'>
|
<svg width={size} height={size} viewBox='0 0 20 20' fill='none' aria-hidden='true'>
|
||||||
<rect x='2.75' y='3.25' width='4' height='13.5' rx='1' stroke={mutedColor} strokeWidth='1.25' />
|
<rect x='2.75' y='3.25' width='4' height='13.5' rx='1' stroke='currentColor' strokeWidth='1.25' opacity='.45' />
|
||||||
<rect x='8' y='3.25' width='4' height='13.5' rx='1' stroke={color} strokeWidth='1.25' />
|
<rect x='8' y='3.25' width='4' height='13.5' rx='1' stroke='currentColor' strokeWidth='1.25' />
|
||||||
<rect x='13.25' y='3.25' width='4' height='13.5' rx='1' stroke={mutedColor} strokeWidth='1.25' />
|
<rect x='13.25' y='3.25' width='4' height='13.5' rx='1' stroke='currentColor' strokeWidth='1.25' opacity='.45' />
|
||||||
<path d='m8.9 10 1 1 1.7-2' stroke={color} strokeWidth='1.25' strokeLinecap='round' strokeLinejoin='round' />
|
<path d='m8.9 10 1 1 1.7-2' stroke='currentColor' strokeWidth='1.25' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AllStageColumns = ({ size = 20, color = '#258141' }) => (
|
export const AllStageColumns = ({ size = 20 }) => (
|
||||||
<svg width={size} height={size} viewBox='0 0 20 20' fill='none' aria-hidden='true'>
|
<svg width={size} height={size} viewBox='0 0 20 20' fill='none' aria-hidden='true'>
|
||||||
<rect x='2.75' y='3.25' width='4' height='13.5' rx='1' stroke={color} strokeWidth='1.25' />
|
<rect x='2.75' y='3.25' width='4' height='13.5' rx='1' stroke='currentColor' strokeWidth='1.25' />
|
||||||
<rect x='8' y='3.25' width='4' height='13.5' rx='1' stroke={color} strokeWidth='1.25' />
|
<rect x='8' y='3.25' width='4' height='13.5' rx='1' stroke='currentColor' strokeWidth='1.25' />
|
||||||
<rect x='13.25' y='3.25' width='4' height='13.5' rx='1' stroke={color} strokeWidth='1.25' />
|
<rect x='13.25' y='3.25' width='4' height='13.5' rx='1' stroke='currentColor' strokeWidth='1.25' />
|
||||||
<path d='M4.25 10h1M9.5 10h1M14.75 10h1' stroke={color} strokeWidth='1.25' strokeLinecap='round' />
|
<path d='M4.25 10h1M9.5 10h1M14.75 10h1' stroke='currentColor' strokeWidth='1.25' strokeLinecap='round' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -127,18 +127,37 @@ const NavigationProjectPage = () => {
|
|||||||
try {
|
try {
|
||||||
const offset = archivedPagination.pageIndex * archivedPagination.pageSize;
|
const offset = archivedPagination.pageIndex * archivedPagination.pageSize;
|
||||||
const params = {
|
const params = {
|
||||||
status_in: 'archived',
|
status: 'archived',
|
||||||
offset,
|
offset,
|
||||||
limit: archivedPagination.pageSize,
|
limit: archivedPagination.pageSize,
|
||||||
...(selectedBranch?.id ? { branch_id: selectedBranch.id } : {}),
|
...(selectedBranch?.id ? { branch_id: selectedBranch.id } : {}),
|
||||||
...(searchQuery.trim() ? { search: searchQuery.trim() } : {}),
|
...(searchQuery.trim() ? { search: searchQuery.trim() } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: после реализации серверных фильтров и пагинации достаточно
|
||||||
|
// передать params в listWithReports и использовать response.result/count.
|
||||||
|
// Пока запрос возвращает полный набор, а код ниже имитирует ответ бэка.
|
||||||
const response = await ProjectsApi.listWithReports(params);
|
const response = await ProjectsApi.listWithReports(params);
|
||||||
|
const query = searchQuery.toLowerCase().trim();
|
||||||
|
const filtered = (response.result || [])
|
||||||
|
.filter((item) => item.status === 'archived')
|
||||||
|
.filter((item) => !selectedBranch || item.org_unit_id === selectedBranch.id)
|
||||||
|
.map((item) => {
|
||||||
|
if (!query) return item;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
sub_rows: item.sub_rows?.filter((subItem) =>
|
||||||
|
(subItem.project || '').toLowerCase().includes(query),
|
||||||
|
) || [],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item) => !query
|
||||||
|
|| (item.name || '').toLowerCase().includes(query)
|
||||||
|
|| item.sub_rows.length > 0);
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
setArchivedData(response.result || []);
|
setArchivedData(filtered.slice(offset, offset + archivedPagination.pageSize));
|
||||||
setArchivedTotalCount(response.count || 0);
|
setArchivedTotalCount(filtered.length);
|
||||||
}
|
}
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
console.error('Error loading archived projects:', requestError);
|
console.error('Error loading archived projects:', requestError);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user