Merge branch 'develop'

This commit is contained in:
lizerk 2026-08-04 12:28:34 +03:00
commit 33fd880727
7 changed files with 27 additions and 7 deletions

View File

@ -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:

View File

@ -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": (

View File

@ -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,

View File

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
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.

View File

@ -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<number, string> = {
};
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,

View File

@ -898,8 +898,8 @@ export function PayrollAdminWorkspace({
)}
{periodModalOpen ? <PayrollPeriodFormModal isOpen period={editingPeriod} periods={workspace.periods} onClose={() => 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 ? <PayrollRateFormModal isOpen period={selectedPeriod} employees={workspace.employees} rate={editingRate} onClose={() => setRateModalOpen(false)} onSubmit={saveRate} onSaved={async () => { setRateModalOpen(false); await refreshView("Черновик ставки сохранён."); }} onStale={handleStale} /> : null}
{selectedPeriod && bulkPayRateModalOpen ? <PayrollBulkPayRateModal isOpen period={selectedPeriod} employees={workspace.employees} rates={rates} onClose={() => 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 ? <PayrollRateFormModal isOpen period={selectedPeriod} employees={workspace.rate_employees} rate={editingRate} onClose={() => setRateModalOpen(false)} onSubmit={saveRate} onSaved={async () => { setRateModalOpen(false); await refreshView("Черновик ставки сохранён."); }} onStale={handleStale} /> : null}
{selectedPeriod && bulkPayRateModalOpen ? <PayrollBulkPayRateModal isOpen period={selectedPeriod} employees={workspace.rate_employees} rates={rates} onClose={() => 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 ? <PayrollBulkPointRateModal isOpen period={selectedPeriod} rates={rates} onClose={() => 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 ? <PayrollBulkTargetPointsModal isOpen period={selectedPeriod} employees={workspace.employees} records={workRecords} onClose={() => 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 ? <PayrollWorkRecordFormModal isOpen period={selectedPeriod} employees={workspace.employees} record={editingWork} onClose={() => setWorkModalOpen(false)} onSubmit={saveWork} onSaved={async () => { setWorkModalOpen(false); await refreshView("Черновик выработки сохранён."); }} onStale={handleStale} /> : null}

View File

@ -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;