From 7f6de0d286b6c4a480e0a485cef4314492d05175 Mon Sep 17 00:00:00 2001 From: lizerk Date: Tue, 4 Aug 2026 09:30:31 +0300 Subject: [PATCH 1/2] fix(finance): allow pay rates for inactive employees --- backend/api/v1/finance/admin_serializers.py | 2 +- backend/api/v1/finance/admin_views.py | 10 ++++++++++ backend/tests/finance/test_payroll_admin_api.py | 9 ++++++++- frontend/next-env.d.ts | 2 +- .../components/finance/admin/PayrollAdminWorkspace.tsx | 4 ++-- frontend/src/lib/api/finance.ts | 1 + 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/backend/api/v1/finance/admin_serializers.py b/backend/api/v1/finance/admin_serializers.py index 0659ff60..4d1570de 100644 --- a/backend/api/v1/finance/admin_serializers.py +++ b/backend/api/v1/finance/admin_serializers.py @@ -252,7 +252,7 @@ class EmployeePayRateWriteSerializer( expected_lock_version = DraftWriteSerializerMixin.expected_lock_version employee_id = serializers.PrimaryKeyRelatedField( source="employee", - queryset=Employee.objects.filter(is_active=True), + queryset=Employee.objects.all(), ) class Meta: diff --git a/backend/api/v1/finance/admin_views.py b/backend/api/v1/finance/admin_views.py index 37116535..8fbf145e 100644 --- a/backend/api/v1/finance/admin_views.py +++ b/backend/api/v1/finance/admin_views.py @@ -268,6 +268,12 @@ class PayrollAdminWorkspaceView(PayrollAdminAPIView): .prefetch_related("departments_links__department") .order_by("last_name", "first_name", "id") ) + rate_employees = ( + Employee.objects.all() + .select_related("position") + .prefetch_related("departments_links__department") + .order_by("last_name", "first_name", "id") + ) components = PayrollComponent.objects.order_by("display_order", "code") current_run = None @@ -456,6 +462,10 @@ class PayrollAdminWorkspaceView(PayrollAdminAPIView): { "permissions": permissions_payload, "employees": PayrollEmployeeMiniSerializer(employees, many=True).data, + "rate_employees": PayrollEmployeeMiniSerializer( + rate_employees, + many=True, + ).data, "components": PayrollComponentSerializer(components, many=True).data, "periods": PayrollPeriodSerializer(periods, many=True).data, "selected_period": ( diff --git a/backend/tests/finance/test_payroll_admin_api.py b/backend/tests/finance/test_payroll_admin_api.py index b88387ee..530f4d6b 100644 --- a/backend/tests/finance/test_payroll_admin_api.py +++ b/backend/tests/finance/test_payroll_admin_api.py @@ -150,6 +150,10 @@ def test_workspace_contract_is_minimal_and_redacts_money_without_view_all( first_name="Мария", last_name="Расчётова", ) + inactive_employee = user_factory( + email="workspace.dismissed@example.test", + active=False, + ) grant(manager, "manage_payroll_inputs") response = auth_client_factory(manager).get(api_url("admin-workspace")) @@ -184,6 +188,8 @@ def test_workspace_contract_is_minimal_and_redacts_money_without_view_all( "position": None, "department": None, } + assert inactive_employee.pk not in {item["id"] for item in body["employees"]} + assert inactive_employee.pk in {item["id"] for item in body["rate_employees"]} def test_period_table_lists_all_employees_and_requires_view_all( @@ -1120,7 +1126,8 @@ def test_period_and_rate_drafts_use_exact_optimistic_versions_and_maker_checker( }, format="json", ) - assert inactive_response.status_code == 400 + assert inactive_response.status_code == 201, inactive_response.content + assert inactive_response.json()["employee_id"] == inactive_employee.pk created = maker_client.post( rate_list_url, diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts index 9edff1c7..c4b7818f 100644 --- a/frontend/next-env.d.ts +++ b/frontend/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/frontend/src/components/finance/admin/PayrollAdminWorkspace.tsx b/frontend/src/components/finance/admin/PayrollAdminWorkspace.tsx index 4c661d94..7c6b4caa 100644 --- a/frontend/src/components/finance/admin/PayrollAdminWorkspace.tsx +++ b/frontend/src/components/finance/admin/PayrollAdminWorkspace.tsx @@ -898,8 +898,8 @@ export function PayrollAdminWorkspace({ )} {periodModalOpen ? setPeriodModalOpen(false)} onSubmit={savePeriod} onSaved={async () => { setPeriodModalOpen(false); const createdPeriodId = createdPeriodIdRef.current; createdPeriodIdRef.current = null; if (createdPeriodId) { setNotice("Период создан и выбран."); await loadWorkspace(createdPeriodId, true); setTableVersion((value) => value + 1); } else { await refreshView("Параметры периода сохранены."); } }} onStale={handleStale} /> : null} - {selectedPeriod && rateModalOpen ? setRateModalOpen(false)} onSubmit={saveRate} onSaved={async () => { setRateModalOpen(false); await refreshView("Черновик ставки сохранён."); }} onStale={handleStale} /> : null} - {selectedPeriod && bulkPayRateModalOpen ? setBulkPayRateModalOpen(false)} onSubmit={(payload) => apiClient.bulkCreatePayrollAdminPayRates(selectedPeriod.id, payload)} onSaved={async (result: PayrollBulkPayRateResult) => { setBulkPayRateModalOpen(false); const changed = result.summary.created_drafts + result.summary.updated_drafts + result.summary.created_revisions; const details = [result.summary.unchanged ? `без изменений: ${result.summary.unchanged}` : "", result.summary.skipped ? `пропущено: ${result.summary.skipped}` : ""].filter(Boolean).join(", "); await refreshView(`Ставка сохранена для ${changed} сотрудников${details ? ` (${details})` : ""}.`); }} onStale={handleStale} /> : null} + {selectedPeriod && rateModalOpen ? setRateModalOpen(false)} onSubmit={saveRate} onSaved={async () => { setRateModalOpen(false); await refreshView("Черновик ставки сохранён."); }} onStale={handleStale} /> : null} + {selectedPeriod && bulkPayRateModalOpen ? setBulkPayRateModalOpen(false)} onSubmit={(payload) => apiClient.bulkCreatePayrollAdminPayRates(selectedPeriod.id, payload)} onSaved={async (result: PayrollBulkPayRateResult) => { setBulkPayRateModalOpen(false); const changed = result.summary.created_drafts + result.summary.updated_drafts + result.summary.created_revisions; const details = [result.summary.unchanged ? `без изменений: ${result.summary.unchanged}` : "", result.summary.skipped ? `пропущено: ${result.summary.skipped}` : ""].filter(Boolean).join(", "); await refreshView(`Ставка сохранена для ${changed} сотрудников${details ? ` (${details})` : ""}.`); }} onStale={handleStale} /> : null} {selectedPeriod && bulkPointRateModalOpen ? setBulkPointRateModalOpen(false)} onSubmit={(payload) => apiClient.bulkSetPayrollAdminPointRate(selectedPeriod.id, payload)} onSaved={async (result: PayrollBulkPointRateResult) => { setBulkPointRateModalOpen(false); const changed = result.summary.updated_drafts + result.summary.created_revisions; const details = [result.summary.unchanged ? `без изменений: ${result.summary.unchanged}` : "", result.summary.skipped ? `пропущено: ${result.summary.skipped}` : ""].filter(Boolean).join(", "); await refreshView(`Цена балла сверх нормы сохранена для ${changed} ставок${details ? ` (${details})` : ""}.`); }} onStale={handleStale} /> : null} {selectedPeriod && bulkTargetPointsModalOpen ? setBulkTargetPointsModalOpen(false)} onSubmit={(payload) => apiClient.bulkSetPayrollAdminTargetPoints(selectedPeriod.id, payload)} onSaved={async (result: PayrollBulkTargetPointsResult) => { setBulkTargetPointsModalOpen(false); const changed = result.summary.created_drafts + result.summary.updated_drafts + result.summary.created_revisions; const details = [result.summary.unchanged ? `без изменений: ${result.summary.unchanged}` : "", result.summary.skipped ? `пропущено: ${result.summary.skipped}` : ""].filter(Boolean).join(", "); await refreshView(`Норма баллов сохранена для ${changed} сотрудников${details ? ` (${details})` : ""}.`); }} onStale={handleStale} /> : null} {selectedPeriod && workModalOpen ? setWorkModalOpen(false)} onSubmit={saveWork} onSaved={async () => { setWorkModalOpen(false); await refreshView("Черновик выработки сохранён."); }} onStale={handleStale} /> : null} diff --git a/frontend/src/lib/api/finance.ts b/frontend/src/lib/api/finance.ts index ade4132a..c88afacc 100644 --- a/frontend/src/lib/api/finance.ts +++ b/frontend/src/lib/api/finance.ts @@ -194,6 +194,7 @@ export type PayrollCalculationBlocker = { export type PayrollAdminWorkspace = { permissions: PayrollAdminPermissions; employees: PayrollAdminEmployee[]; + rate_employees: PayrollAdminEmployee[]; components: PayrollComponent[]; periods: PayrollAdminPeriod[]; selected_period: PayrollAdminPeriod | null; From 91ca6b55e8a24249d784c86fe7d9aa05e8aa8a57 Mon Sep 17 00:00:00 2001 From: lizerk Date: Tue, 4 Aug 2026 12:28:13 +0300 Subject: [PATCH 2/2] fix(attendance): align matrix pagination with modal width --- frontend/src/app/attendance/page.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/attendance/page.tsx b/frontend/src/app/attendance/page.tsx index 9414c472..6e274579 100644 --- a/frontend/src/app/attendance/page.tsx +++ b/frontend/src/app/attendance/page.tsx @@ -61,7 +61,8 @@ const weekdays = [ const defaultWorkdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]; const matrixDateColumnWidth = 140; const matrixEmployeeColumnWidth = 160; -const matrixModalHorizontalPadding = 96; +const matrixModalMaxWidth = 1152; +const matrixModalHorizontalPadding = 48; type AttendancePeriodPreset = "month" | "custom"; type EmployeeQuery = { @@ -611,9 +612,10 @@ const DAYS_RU_FROM_INDEX: Record = { }; function calculateMatrixEmployeesPerPage(width: number) { + const modalWidth = Math.min(width * 0.95, matrixModalMaxWidth); const availableWidth = Math.max( 0, - width - matrixModalHorizontalPadding - matrixDateColumnWidth, + modalWidth - matrixModalHorizontalPadding - matrixDateColumnWidth, ); return Math.min( 8,