Control employee work entry by period
All checks were successful
Deploy production / deploy (push) Successful in 10m54s
All checks were successful
Deploy production / deploy (push) Successful in 10m54s
This commit is contained in:
parent
70ce84c8a7
commit
fc534e3bf3
@ -487,6 +487,38 @@ class PayrollAdminPeriodDetailView(PayrollAdminAPIView):
|
||||
period = get_object_or_404(PayrollPeriod, pk=pk)
|
||||
return Response(PayrollPeriodSerializer(period).data)
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, pk):
|
||||
value = request.data.get("employee_work_entry_locked")
|
||||
if not isinstance(value, bool):
|
||||
raise ValidationError(
|
||||
{"employee_work_entry_locked": "Укажите true или false."}
|
||||
)
|
||||
period = get_object_or_404(PayrollPeriod.objects.select_for_update(), pk=pk)
|
||||
if period.status != PayrollPeriodStatus.OPEN:
|
||||
_conflict(
|
||||
"WORK_PERIOD_LOCKED",
|
||||
"Переключать ввод можно только для открытого периода.",
|
||||
)
|
||||
if period.employee_work_entry_locked != value:
|
||||
period.employee_work_entry_locked = value
|
||||
period.lock_version += 1
|
||||
period.save(
|
||||
update_fields=[
|
||||
"employee_work_entry_locked",
|
||||
"lock_version",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
_audit_api_mutation(
|
||||
actor=request.user,
|
||||
action="payroll.employee_work_entry_lock_changed",
|
||||
instance=period,
|
||||
period=period,
|
||||
metadata={"locked": value},
|
||||
)
|
||||
return Response(PayrollPeriodSerializer(period).data)
|
||||
|
||||
@transaction.atomic
|
||||
def patch(self, request, pk):
|
||||
self.require_permission(request, PAYROLL_PERMISSIONS["manage_inputs"])
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Count, Max
|
||||
from django.db.models import Count, Max, OuterRef, Subquery
|
||||
|
||||
from communications.models import Message
|
||||
from finance.balance_adjustments.services import locked_periods
|
||||
@ -53,7 +53,20 @@ def _cell_comment_summaries(period):
|
||||
is_deleted=False,
|
||||
)
|
||||
.values("chat__context_object_id")
|
||||
.annotate(count=Count("id"), latest_at=Max("created_at"))
|
||||
.annotate(
|
||||
count=Count("id"),
|
||||
latest_at=Max("created_at"),
|
||||
latest_text=Subquery(
|
||||
Message.objects.filter(
|
||||
chat__type="comments",
|
||||
chat__context_content_type=content_type,
|
||||
chat__context_object_id=OuterRef("chat__context_object_id"),
|
||||
is_deleted=False,
|
||||
)
|
||||
.order_by("-created_at", "-id")
|
||||
.values("content")[:1]
|
||||
),
|
||||
)
|
||||
)
|
||||
result = []
|
||||
for total in totals:
|
||||
@ -64,6 +77,7 @@ def _cell_comment_summaries(period):
|
||||
"column_id": thread["column_id"],
|
||||
"count": total["count"],
|
||||
"latest_at": total["latest_at"],
|
||||
"latest_text": total["latest_text"],
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
@ -45,6 +45,7 @@ class PayrollPeriodSerializer(serializers.ModelSerializer):
|
||||
"currency",
|
||||
"status",
|
||||
"status_label",
|
||||
"employee_work_entry_locked",
|
||||
"current_run_id",
|
||||
"automatic_input_copy",
|
||||
"balance_rules_version",
|
||||
|
||||
@ -290,6 +290,9 @@ class OwnPayrollWorkRecordView(
|
||||
else PayrollDailyWorkEntry.objects.none()
|
||||
)
|
||||
daily_target_points = PayrollWorkSettings.get_daily_target_points()
|
||||
from finance.payroll.access import can_access_payroll_management
|
||||
|
||||
can_manage_work = can_access_payroll_management(request.user)
|
||||
schedule, schedule_source = resolve_employee_schedule(request.user)
|
||||
period_payloads = []
|
||||
for period in periods:
|
||||
@ -311,9 +314,11 @@ class OwnPayrollWorkRecordView(
|
||||
"pay_date": period.pay_date,
|
||||
"status": period.status,
|
||||
"status_label": period.get_status_display(),
|
||||
"employee_work_entry_locked": period.employee_work_entry_locked,
|
||||
"editable": (
|
||||
period.status == PayrollPeriodStatus.OPEN
|
||||
and period.date_from <= timezone.localdate()
|
||||
and (not period.employee_work_entry_locked or can_manage_work)
|
||||
),
|
||||
"record": (
|
||||
OwnPayrollWorkRecordSerializer(record).data
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("finance", "0042_input_copy_on_period_closure")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="payrollperiod",
|
||||
name="employee_work_entry_locked",
|
||||
field=models.BooleanField(
|
||||
default=False, verbose_name="Ввод выработки сотрудниками заблокирован"
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -112,6 +112,9 @@ class PayrollPeriod(models.Model):
|
||||
validators=[MinValueValidator(1)],
|
||||
)
|
||||
pay_date = models.DateField("Дата выплаты", null=True, blank=True)
|
||||
employee_work_entry_locked = models.BooleanField(
|
||||
"Ввод выработки сотрудниками заблокирован", default=False
|
||||
)
|
||||
currency = models.CharField(
|
||||
"Валюта",
|
||||
max_length=3,
|
||||
|
||||
@ -42,6 +42,7 @@ from payroll_core import (
|
||||
|
||||
from .access import (
|
||||
SELF_APPROVAL_OVERRIDE_PERMISSION,
|
||||
can_access_payroll_management,
|
||||
can_self_approve_payroll,
|
||||
has_payroll_permission,
|
||||
has_simple_admin_access,
|
||||
@ -1804,6 +1805,11 @@ def save_own_daily_work_entry(
|
||||
"Выработка за этот период уже закрыта для изменений.",
|
||||
period_status=period.status,
|
||||
)
|
||||
if period.employee_work_entry_locked and not can_access_payroll_management(actor):
|
||||
_operation_error(
|
||||
"WORK_ENTRY_LOCKED",
|
||||
"Ввод выработки за этот период закрыт. Обратитесь к администрации.",
|
||||
)
|
||||
if not period.date_from <= work_date <= period.date_to:
|
||||
_operation_error(
|
||||
"WORK_DATE_OUTSIDE_PERIOD",
|
||||
|
||||
@ -1572,10 +1572,35 @@ def test_period_table_cell_comments_use_shared_discussion_flow(
|
||||
assert cell_summary[0]["employee_id"] == employee.pk
|
||||
assert cell_summary[0]["column_id"] == "gross_total"
|
||||
assert cell_summary[0]["count"] == 1
|
||||
assert cell_summary[0]["latest_text"] == "Проверить итоговое начисление"
|
||||
assert parse_datetime(cell_summary[0]["latest_at"]) == parse_datetime(
|
||||
created.json()["created_at"]
|
||||
)
|
||||
|
||||
colleague_client = auth_client_factory(colleague)
|
||||
second = colleague_client.post(
|
||||
comments_url, {"text": "Новое уточнение"}, format="json"
|
||||
)
|
||||
assert second.status_code == 201
|
||||
summary = colleague_client.get(api_url("admin-period-table", pk=period.pk)).json()[
|
||||
"cell_comments"
|
||||
][0]
|
||||
assert summary["count"] == 2
|
||||
assert summary["latest_text"] == "Новое уточнение"
|
||||
second_url = api_url(
|
||||
"admin-cell-comment-detail",
|
||||
pk=period.pk,
|
||||
employee_id=employee.pk,
|
||||
column_id="gross_total",
|
||||
comment_id=second.json()["id"],
|
||||
)
|
||||
assert colleague_client.delete(second_url).status_code == 204
|
||||
summary = colleague_client.get(api_url("admin-period-table", pk=period.pk)).json()[
|
||||
"cell_comments"
|
||||
][0]
|
||||
assert summary["count"] == 1
|
||||
assert summary["latest_text"] == "Проверить итоговое начисление"
|
||||
|
||||
detail_url = api_url(
|
||||
"admin-cell-comment-detail",
|
||||
pk=period.pk,
|
||||
|
||||
@ -7,6 +7,7 @@ from django.utils import timezone
|
||||
|
||||
from attendance.models import EmployeeWorkSchedule, StandardWorkSchedule
|
||||
from finance.models import (
|
||||
FinanceSettings,
|
||||
PayrollAuditEvent,
|
||||
PayrollDailyWorkEntry,
|
||||
PayrollPeriod,
|
||||
@ -314,6 +315,108 @@ def test_daily_work_rejects_locked_period_and_date_outside_period(
|
||||
assert not PayrollDailyWorkEntry.objects.filter(employee=employee).exists()
|
||||
|
||||
|
||||
def test_management_can_lock_and_unlock_employee_work_entry(
|
||||
user_factory, auth_client_factory, settings
|
||||
):
|
||||
settings.FINANCE_PAYROLL = {"SIMPLE_ADMIN_ACCESS": True}
|
||||
employee = user_factory(email="work.lock.employee@example.test")
|
||||
manager = user_factory(email="work.lock.manager@example.test", staff=True)
|
||||
outsider = user_factory(email="work.lock.outsider@example.test", staff=True)
|
||||
FinanceSettings.get_solo().management_users.set([manager])
|
||||
period = make_period(creator=manager)
|
||||
user_client = auth_client_factory(employee)
|
||||
manager_client = auth_client_factory(manager)
|
||||
outsider_client = auth_client_factory(outsider)
|
||||
admin_url = reverse(
|
||||
"api:v1:finance-payroll:admin-period-detail", kwargs={"pk": period.pk}
|
||||
)
|
||||
entry_payload = {
|
||||
"period_id": period.pk,
|
||||
"work_date": "2026-06-02",
|
||||
"actual_points": "5",
|
||||
}
|
||||
|
||||
created = user_client.post(api_url(), entry_payload, format="json")
|
||||
assert created.status_code == 201, created.content
|
||||
entry_id = created.json()["entry"]["id"]
|
||||
assert (
|
||||
outsider_client.post(
|
||||
admin_url, {"employee_work_entry_locked": True}, format="json"
|
||||
).status_code
|
||||
== 403
|
||||
)
|
||||
assert (
|
||||
manager_client.post(
|
||||
admin_url, {"employee_work_entry_locked": True}, format="json"
|
||||
).status_code
|
||||
== 200
|
||||
)
|
||||
|
||||
period.refresh_from_db()
|
||||
assert period.employee_work_entry_locked is True
|
||||
assert period.status == "open"
|
||||
assert period.current_run_id is None
|
||||
assert period.component_snapshot == {}
|
||||
employee_period = user_client.get(api_url(), {"period_id": period.pk}).json()[
|
||||
"periods"
|
||||
][0]
|
||||
manager_period = manager_client.get(api_url(), {"period_id": period.pk}).json()[
|
||||
"periods"
|
||||
][0]
|
||||
assert employee_period["editable"] is False
|
||||
assert manager_period["editable"] is True
|
||||
blocked = user_client.post(
|
||||
api_url(),
|
||||
{**entry_payload, "actual_points": "8", "expected_lock_version": 0},
|
||||
format="json",
|
||||
)
|
||||
assert blocked.status_code == 409
|
||||
assert blocked.json()["code"] == "WORK_ENTRY_LOCKED"
|
||||
assert PayrollDailyWorkEntry.objects.get(pk=entry_id).actual_points == Decimal("5")
|
||||
blocked_new = user_client.post(
|
||||
api_url(), {**entry_payload, "work_date": "2026-06-03"}, format="json"
|
||||
)
|
||||
assert blocked_new.status_code == 409
|
||||
assert not PayrollDailyWorkEntry.objects.filter(
|
||||
employee=employee, work_date="2026-06-03"
|
||||
).exists()
|
||||
manager_entry = manager_client.post(api_url(), entry_payload, format="json")
|
||||
assert manager_entry.status_code == 201, manager_entry.content
|
||||
|
||||
assert (
|
||||
manager_client.post(
|
||||
admin_url, {"employee_work_entry_locked": False}, format="json"
|
||||
).status_code
|
||||
== 200
|
||||
)
|
||||
saved = user_client.post(
|
||||
api_url(),
|
||||
{**entry_payload, "actual_points": "8", "expected_lock_version": 0},
|
||||
format="json",
|
||||
)
|
||||
assert saved.status_code == 200, saved.content
|
||||
assert PayrollDailyWorkEntry.objects.get(pk=entry_id).actual_points == Decimal("8")
|
||||
assert (
|
||||
PayrollAuditEvent.objects.filter(
|
||||
period=period, action="payroll.employee_work_entry_lock_changed"
|
||||
).count()
|
||||
== 2
|
||||
)
|
||||
period.refresh_from_db()
|
||||
period.status = "closed"
|
||||
period.component_snapshot = {"columns": [{"code": "SNAPSHOT"}]}
|
||||
period.save(update_fields=["status", "component_snapshot"])
|
||||
assert (
|
||||
manager_client.post(
|
||||
admin_url, {"employee_work_entry_locked": True}, format="json"
|
||||
).status_code
|
||||
== 409
|
||||
)
|
||||
period.refresh_from_db()
|
||||
assert period.component_snapshot == {"columns": [{"code": "SNAPSHOT"}]}
|
||||
assert period.employee_work_entry_locked is False
|
||||
|
||||
|
||||
def test_global_daily_target_applies_to_new_entries_and_preserves_history(
|
||||
user_factory,
|
||||
auth_client_factory,
|
||||
|
||||
@ -1539,7 +1539,7 @@ export default function ProcurementPage() {
|
||||
}
|
||||
className="h-4 w-4 rounded accent-[var(--accent-primary)]"
|
||||
/>
|
||||
<Flag size={14} className="text-amber-500" />
|
||||
<Flag size={14} className="text-red-500" />
|
||||
Отложенные мной
|
||||
</label>
|
||||
{activeFilterCount > 0 && (
|
||||
@ -1856,6 +1856,7 @@ export default function ProcurementPage() {
|
||||
>
|
||||
<Flag
|
||||
size={15}
|
||||
className="text-red-500"
|
||||
fill={req.is_deferred ? "currentColor" : "none"}
|
||||
/>
|
||||
</button>
|
||||
@ -1946,6 +1947,7 @@ export default function ProcurementPage() {
|
||||
>
|
||||
<Flag
|
||||
size={15}
|
||||
className="text-red-500"
|
||||
fill={
|
||||
selectedRequest.is_deferred
|
||||
? "currentColor"
|
||||
@ -2589,6 +2591,7 @@ export default function ProcurementPage() {
|
||||
>
|
||||
<Flag
|
||||
size={15}
|
||||
className="text-red-500"
|
||||
fill={
|
||||
req.is_deferred
|
||||
? "currentColor"
|
||||
|
||||
@ -643,7 +643,10 @@ export function PayrollDailyWorkSection({
|
||||
|
||||
{!selectedPeriod?.editable ? (
|
||||
<div className="app-feedback-warning mt-4 rounded-lg px-3 py-2.5 text-sm">
|
||||
Период закрыт для изменения выработки.
|
||||
{selectedPeriod?.employee_work_entry_locked &&
|
||||
selectedPeriod.status === "open"
|
||||
? "Ввод выработки за этот период закрыт. Обратитесь к администрации."
|
||||
: "Период закрыт для изменения выработки."}
|
||||
</div>
|
||||
) : null}
|
||||
{error ? (
|
||||
|
||||
@ -14,6 +14,8 @@ import {
|
||||
FileCheck2,
|
||||
FileSpreadsheet,
|
||||
Loader2,
|
||||
LockKeyhole,
|
||||
LockKeyholeOpen,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
Pencil,
|
||||
@ -678,6 +680,8 @@ export function PayrollAdminWorkspace({
|
||||
const [tableError, setTableError] = useState<string | null>(null);
|
||||
const [tableVersion, setTableVersion] = useState(0);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
const [workEntryLockBusy, setWorkEntryLockBusy] = useState(false);
|
||||
const [workEntryLockError, setWorkEntryLockError] = useState<string | null>(null);
|
||||
|
||||
const [periodModalOpen, setPeriodModalOpen] = useState(false);
|
||||
const [editingPeriod, setEditingPeriod] = useState<PayrollAdminPeriod | null>(
|
||||
@ -1734,6 +1738,57 @@ export function PayrollAdminWorkspace({
|
||||
selectedPeriodId={selectedPeriod?.id}
|
||||
onChange={(periodId) => void loadWorkspace(periodId, true)}
|
||||
/>
|
||||
{selectedPeriod?.status === "open" ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={workEntryLockBusy}
|
||||
aria-pressed={Boolean(selectedPeriod.employee_work_entry_locked)}
|
||||
title={
|
||||
selectedPeriod.employee_work_entry_locked
|
||||
? "Разрешить сотрудникам ввод выработки"
|
||||
: "Закрыть сотрудникам ввод выработки"
|
||||
}
|
||||
aria-label={
|
||||
selectedPeriod.employee_work_entry_locked
|
||||
? "Разрешить сотрудникам ввод выработки"
|
||||
: "Закрыть сотрудникам ввод выработки"
|
||||
}
|
||||
className={`inline-flex h-10 items-center gap-2 rounded-lg px-3 text-sm disabled:opacity-50 ${selectedPeriod.employee_work_entry_locked ? "app-selected" : "app-action-secondary"}`}
|
||||
onClick={async () => {
|
||||
const locked = !selectedPeriod.employee_work_entry_locked;
|
||||
setWorkEntryLockBusy(true);
|
||||
setWorkEntryLockError(null);
|
||||
try {
|
||||
await apiClient.setPayrollEmployeeWorkEntryLock(
|
||||
selectedPeriod.id,
|
||||
locked,
|
||||
);
|
||||
await refreshView(
|
||||
locked
|
||||
? "Ввод выработки сотрудниками закрыт."
|
||||
: "Ввод выработки сотрудниками открыт.",
|
||||
);
|
||||
} catch (error) {
|
||||
setWorkEntryLockError(
|
||||
getPayrollAdminError(error, "Не удалось изменить доступ к выработке."),
|
||||
);
|
||||
} finally {
|
||||
setWorkEntryLockBusy(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedPeriod.employee_work_entry_locked ? (
|
||||
<LockKeyhole size={16} />
|
||||
) : (
|
||||
<LockKeyholeOpen size={16} />
|
||||
)}
|
||||
<span className="hidden xl:inline">
|
||||
{selectedPeriod.employee_work_entry_locked
|
||||
? "Выработка закрыта"
|
||||
: "Выработка открыта"}
|
||||
</span>
|
||||
</button>
|
||||
) : null}
|
||||
{selectedPeriod?.status === "open" &&
|
||||
permissions.manage_inputs &&
|
||||
!workspace.current_run ? (
|
||||
@ -1838,6 +1893,12 @@ export function PayrollAdminWorkspace({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{workEntryLockError ? (
|
||||
<div role="alert" className="app-feedback-danger rounded-xl px-4 py-3 text-sm">
|
||||
{workEntryLockError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div role="tablist" aria-label="Разделы управления зарплатой">
|
||||
<HorizontalChipScroller ariaLabel="Разделы управления зарплатой">
|
||||
{visibleTabs.map((item) => {
|
||||
|
||||
@ -30,7 +30,11 @@ type PayrollCellCommentsModalProps = {
|
||||
cell: PayrollCommentCell | null;
|
||||
currentUserId?: number | null;
|
||||
onClose: () => void;
|
||||
onCommentCountChange: (cell: PayrollCommentCell, count: number) => void;
|
||||
onCommentCountChange: (
|
||||
cell: PayrollCommentCell,
|
||||
count: number,
|
||||
latestText: string,
|
||||
) => void;
|
||||
periodId: number;
|
||||
};
|
||||
|
||||
@ -71,7 +75,7 @@ export function PayrollCellCommentsModal({
|
||||
activeCell.columnId,
|
||||
);
|
||||
setComments(next);
|
||||
onCommentCountChange(activeCell, next.length);
|
||||
onCommentCountChange(activeCell, next.length, next.at(-1)?.text || "");
|
||||
} catch (loadError) {
|
||||
setComments([]);
|
||||
setError(
|
||||
@ -129,7 +133,7 @@ export function PayrollCellCommentsModal({
|
||||
const next = [...comments, saved];
|
||||
setComments(next);
|
||||
setDraft("");
|
||||
onCommentCountChange(cell, next.length);
|
||||
onCommentCountChange(cell, next.length, next.at(-1)?.text || "");
|
||||
} catch (sendError) {
|
||||
setError(
|
||||
getPayrollAdminError(sendError, "Не удалось отправить комментарий."),
|
||||
@ -153,7 +157,7 @@ export function PayrollCellCommentsModal({
|
||||
);
|
||||
const next = comments.filter((item) => item.id !== comment.id);
|
||||
setComments(next);
|
||||
onCommentCountChange(cell, next.length);
|
||||
onCommentCountChange(cell, next.length, next.at(-1)?.text || "");
|
||||
} catch (deleteError) {
|
||||
setError(
|
||||
getPayrollAdminError(deleteError, "Не удалось удалить комментарий."),
|
||||
|
||||
@ -1681,10 +1681,12 @@ function CellCommentButton({
|
||||
count,
|
||||
label,
|
||||
onClick,
|
||||
preview,
|
||||
}: {
|
||||
count: number;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
preview?: string;
|
||||
}) {
|
||||
if (count) {
|
||||
return (
|
||||
@ -1696,7 +1698,7 @@ function CellCommentButton({
|
||||
}}
|
||||
className="absolute right-1 top-1 z-10 rounded border border-sky-500/60 bg-sky-500/15 px-1 text-[9px] font-semibold leading-3 text-sky-400 transition hover:bg-sky-500/25 hover:text-sky-300"
|
||||
aria-label={`${label}: открыть комментарии, ${count}`}
|
||||
title="Открыть комментарии"
|
||||
title={preview || "Открыть комментарии"}
|
||||
>
|
||||
К:{count}
|
||||
</button>
|
||||
@ -1724,12 +1726,14 @@ function CommentableValueCell({
|
||||
commentCount,
|
||||
commentLabel,
|
||||
onOpenComment,
|
||||
commentPreview,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
commentCount: number;
|
||||
commentLabel: string;
|
||||
onOpenComment: () => void;
|
||||
commentPreview?: string;
|
||||
}) {
|
||||
return (
|
||||
<td className={`${bodyCellClass} relative ${className}`}>
|
||||
@ -1737,6 +1741,7 @@ function CommentableValueCell({
|
||||
count={commentCount}
|
||||
label={commentLabel}
|
||||
onClick={onOpenComment}
|
||||
preview={commentPreview}
|
||||
/>
|
||||
{children}
|
||||
</td>
|
||||
@ -1759,6 +1764,7 @@ function ProjectionPointsCell({
|
||||
onOpen,
|
||||
commentCount,
|
||||
onOpenComment,
|
||||
commentPreview,
|
||||
cellClassName = "",
|
||||
}: {
|
||||
value: string | null;
|
||||
@ -1767,6 +1773,7 @@ function ProjectionPointsCell({
|
||||
onOpen: () => void;
|
||||
commentCount: number;
|
||||
onOpenComment: () => void;
|
||||
commentPreview?: string;
|
||||
cellClassName?: string;
|
||||
}) {
|
||||
const difference = comparePayrollProjection(value, actualPoints);
|
||||
@ -1790,6 +1797,7 @@ function ProjectionPointsCell({
|
||||
count={commentCount}
|
||||
label={label}
|
||||
onClick={onOpenComment}
|
||||
preview={commentPreview}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@ -1828,6 +1836,7 @@ function InlineEditableCell({
|
||||
automatic = false,
|
||||
commentCount,
|
||||
onOpenComment,
|
||||
commentPreview,
|
||||
cellClassName = "",
|
||||
annotation,
|
||||
}: {
|
||||
@ -1838,6 +1847,7 @@ function InlineEditableCell({
|
||||
automatic?: boolean;
|
||||
commentCount: number;
|
||||
onOpenComment: () => void;
|
||||
commentPreview?: string;
|
||||
cellClassName?: string;
|
||||
annotation?: { label: string; title: string };
|
||||
}) {
|
||||
@ -1854,6 +1864,7 @@ function InlineEditableCell({
|
||||
commentCount={commentCount}
|
||||
commentLabel={label}
|
||||
onOpenComment={onOpenComment}
|
||||
commentPreview={commentPreview}
|
||||
>
|
||||
<span>{formattedValue}</span>
|
||||
{annotation ? (
|
||||
@ -1891,6 +1902,7 @@ function InlineEditableCell({
|
||||
count={commentCount}
|
||||
label={label}
|
||||
onClick={onOpenComment}
|
||||
preview={commentPreview}
|
||||
/>
|
||||
<label className="relative block min-h-10 w-full max-w-full overflow-hidden">
|
||||
<input
|
||||
@ -2039,7 +2051,7 @@ export function PayrollPeriodTableView({
|
||||
const [activeCommentCell, setActiveCommentCell] =
|
||||
useState<PayrollCommentCell | null>(null);
|
||||
const [commentCountOverrides, setCommentCountOverrides] = useState<
|
||||
Record<string, { count: number; periodId: number }>
|
||||
Record<string, { count: number; periodId: number; latestText: string }>
|
||||
>({});
|
||||
const [breakdownEmployee, setBreakdownEmployee] = useState<{
|
||||
id: number;
|
||||
@ -2097,12 +2109,13 @@ export function PayrollPeriodTableView({
|
||||
[hiddenRows, searchedRows],
|
||||
);
|
||||
const updateCommentCount = useCallback(
|
||||
(cell: PayrollCommentCell, count: number) => {
|
||||
(cell: PayrollCommentCell, count: number, latestText: string) => {
|
||||
setCommentCountOverrides((current) => ({
|
||||
...current,
|
||||
[commentCellKey(cell.employeeId, cell.columnId)]: {
|
||||
count,
|
||||
periodId: cell.periodId,
|
||||
latestText,
|
||||
},
|
||||
}));
|
||||
},
|
||||
@ -2360,15 +2373,24 @@ export function PayrollPeriodTableView({
|
||||
summary.count,
|
||||
]),
|
||||
);
|
||||
const commentPreviewByCell = new Map(
|
||||
(data.cell_comments || []).map((summary) => [
|
||||
commentCellKey(summary.employee_id, summary.column_id),
|
||||
summary.latest_text || "",
|
||||
]),
|
||||
);
|
||||
Object.entries(commentCountOverrides).forEach(([key, override]) => {
|
||||
if (override.periodId === data.period_id)
|
||||
if (override.periodId === data.period_id) {
|
||||
commentCountByCell.set(key, override.count);
|
||||
commentPreviewByCell.set(key, override.latestText);
|
||||
}
|
||||
});
|
||||
const commentProps = (
|
||||
row: PayrollPeriodTableRow,
|
||||
columnId: string,
|
||||
columnLabel: string,
|
||||
) => ({
|
||||
commentPreview: commentPreviewByCell.get(commentCellKey(row.employee.id, columnId)) || "",
|
||||
commentCount:
|
||||
commentCountByCell.get(commentCellKey(row.employee.id, columnId)) || 0,
|
||||
onOpenComment: () =>
|
||||
@ -3012,6 +3034,7 @@ export function PayrollPeriodTableView({
|
||||
>
|
||||
<CellCommentButton
|
||||
count={comments.commentCount}
|
||||
preview={comments.commentPreview}
|
||||
label={`Остаток к переносу: ${row.employee.display_name}`}
|
||||
onClick={comments.onOpenComment}
|
||||
/>
|
||||
@ -3058,6 +3081,7 @@ export function PayrollPeriodTableView({
|
||||
>
|
||||
<CellCommentButton
|
||||
count={comments.commentCount}
|
||||
preview={comments.commentPreview}
|
||||
label={`Состояние: ${row.employee.display_name}`}
|
||||
onClick={comments.onOpenComment}
|
||||
/>
|
||||
@ -3077,6 +3101,7 @@ export function PayrollPeriodTableView({
|
||||
>
|
||||
<CellCommentButton
|
||||
count={comments.commentCount}
|
||||
preview={comments.commentPreview}
|
||||
label={`Состояние: ${row.employee.display_name}`}
|
||||
onClick={comments.onOpenComment}
|
||||
/>
|
||||
@ -3754,6 +3779,7 @@ export function PayrollPeriodTableView({
|
||||
style={{ left: REVIEW_MARK_COLUMN_WIDTH }}
|
||||
>
|
||||
<CellCommentButton
|
||||
preview={commentPreviewByCell.get(commentCellKey(row.employee.id, "employee"))}
|
||||
count={
|
||||
commentCountByCell.get(
|
||||
commentCellKey(row.employee.id, "employee"),
|
||||
|
||||
@ -110,6 +110,7 @@ export type PayrollOwnDailyWorkEntry = {
|
||||
|
||||
export type PayrollOwnWorkPeriod = PayrollPeriodSummary & {
|
||||
id: number;
|
||||
employee_work_entry_locked?: boolean;
|
||||
status: PayrollPeriodStatus;
|
||||
status_label: string;
|
||||
editable: boolean;
|
||||
@ -207,6 +208,7 @@ export type PayrollManagementAccessSettings = {
|
||||
|
||||
export type PayrollAdminPeriod = PayrollPeriodSummary & {
|
||||
id: number;
|
||||
employee_work_entry_locked?: boolean;
|
||||
currency: string;
|
||||
status: PayrollPeriodStatus;
|
||||
lock_version: number;
|
||||
@ -450,6 +452,7 @@ export type PayrollCellCommentSummary = {
|
||||
column_id: string;
|
||||
count: number;
|
||||
latest_at: string;
|
||||
latest_text?: string;
|
||||
};
|
||||
|
||||
export type PayrollAttendanceAlert = {
|
||||
@ -1413,6 +1416,14 @@ export function createFinanceApi(request: RequestFn) {
|
||||
`${adminPrefix}/periods/${periodId}/`,
|
||||
jsonOptions("PATCH", payload),
|
||||
),
|
||||
setPayrollEmployeeWorkEntryLock: (
|
||||
periodId: number,
|
||||
locked: boolean,
|
||||
): Promise<PayrollAdminPeriod> =>
|
||||
request(
|
||||
`${adminPrefix}/periods/${periodId}/`,
|
||||
jsonOptions("POST", { employee_work_entry_locked: locked }),
|
||||
),
|
||||
getPayrollAdminPayRates: (
|
||||
params: PayrollAdminListParams = {},
|
||||
): Promise<PayrollAdminListPayload<PayrollAdminPayRate>> =>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user