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/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,
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;