Merge branch 'develop'

This commit is contained in:
admin i 2026-09-01 15:59:21 +03:00
commit 411eda9765
3 changed files with 121 additions and 77 deletions

View File

@ -51,9 +51,13 @@ function currentEntryForDate(
}
export function PayrollDailyWorkSection({
compactPeriodSelector = false,
detailsTarget,
embedded = false,
periodSelectorTargetId,
}: {
compactPeriodSelector?: boolean;
detailsTarget?: HTMLElement | null;
embedded?: boolean;
periodSelectorTargetId?: string;
}) {
@ -201,6 +205,7 @@ export function PayrollDailyWorkSection({
const periodStatus = selectedPeriod ? periodStatusMeta[selectedPeriod.status] : null;
const periodSelector = (
<PayrollWorkDatePicker
compact={compactPeriodSelector}
periods={workspace.periods}
selectedPeriod={selectedPeriod}
selectedDate={workDate}
@ -216,6 +221,79 @@ export function PayrollDailyWorkSection({
const cardClassName = embedded
? "app-surface-muted rounded-xl p-4"
: "app-surface rounded-2xl p-4 sm:p-5";
const detailsContent = (
<div className={embedded ? "space-y-3" : "space-y-4"}>
<section className={cardClassName}>
<div className="min-w-0">
<p className="app-card-caption">Выработка</p>
<h2 className="mt-1 text-lg font-semibold text-[var(--foreground)]">Ежедневные баллы</h2>
</div>
{selectedPeriod ? (
<div className="mt-4 grid grid-cols-[repeat(auto-fit,minmax(min(100%,10rem),1fr))] gap-3">
<div className="app-surface-muted rounded-xl p-3">
<p className="app-text-muted text-xs">Период</p>
<p className="mt-1 text-sm font-medium leading-5 text-[var(--foreground)]">{getPayrollPeriodRange(selectedPeriod)}</p>
{periodStatus ? <span className={`app-status-pill mt-2 shrink-0 whitespace-nowrap ${periodStatus.className}`}>{periodStatus.label}</span> : null}
</div>
<div className="app-surface-muted rounded-xl p-3">
<p className="app-text-muted text-xs">Норма за период</p>
<p className="mt-1 text-lg font-semibold text-[var(--foreground)]">{formatPayrollPoints(summary?.target_points, "0")}</p>
<p className="app-text-muted mt-1 text-[11px]">
{summary?.target_source === "saved_record" ? "Из сохранённой записи" : `По графику: ${summary?.workdays_count || 0} дн.`}
</p>
</div>
<div className="app-surface-muted rounded-xl p-3">
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0 grow basis-24">
<p className="app-text-muted text-xs">Фактически за период</p>
<p className="mt-1 text-lg font-semibold text-[var(--foreground)]">
{formatPayrollPoints(summary?.actual_points, "0")} / {formatPayrollPoints(summary?.target_points, "0")}
</p>
</div>
{aggregateStatus ? <span className={`app-status-pill shrink-0 whitespace-nowrap ${aggregateStatus.className}`}>{aggregateStatus.label}</span> : null}
</div>
</div>
</div>
) : null}
</section>
<section className={cardClassName}>
<div className="mb-4 flex items-center justify-between gap-3">
<h2 className="text-base font-semibold text-[var(--foreground)]">Записи за период</h2>
<span className="app-counter h-6 min-w-6 px-1.5 text-xs">{workspace.entries.length}</span>
</div>
{workspace.entries.length ? (
<div className="space-y-2">
{workspace.entries.map((entry) => (
<button
type="button"
key={entry.id}
className={`app-surface-muted grid w-full grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-xl p-3 text-left transition hover:border-[var(--border-strong)] ${entry.work_date === workDate ? "app-selected" : ""}`}
onClick={() => selectWorkDate(entry.work_date)}
>
<span className="min-w-0">
<span className="block text-sm font-semibold text-[var(--foreground)]">{formatPayrollDate(entry.work_date)}</span>
<span className="app-text-muted mt-1 block truncate text-xs">{entry.note || "Без комментария"}</span>
</span>
<span className="flex shrink-0 items-center gap-3">
<span className="text-right">
<span className="block text-sm font-semibold text-[var(--foreground)]">{formatPayrollPoints(entry.actual_points, "0")}</span>
<span className="app-text-muted block text-[11px]">из {formatPayrollPoints(entry.target_points, "0")}</span>
</span>
{selectedPeriod?.editable ? <Pencil className="app-text-muted" size={15} /> : null}
</span>
</button>
))}
</div>
) : (
<div className="app-surface-muted rounded-xl px-4 py-8 text-center">
<p className="app-text-muted text-sm">За выбранный период записей пока нет.</p>
</div>
)}
</section>
</div>
);
return (
<div className={embedded ? "space-y-3" : "space-y-4"}>
@ -299,75 +377,9 @@ export function PayrollDailyWorkSection({
</form>
</section>
<section className={cardClassName}>
<div className="min-w-0">
<p className="app-card-caption">Выработка</p>
<h2 className="mt-1 text-lg font-semibold text-[var(--foreground)]">Ежедневные баллы</h2>
</div>
{selectedPeriod ? (
<div className="mt-4 grid grid-cols-[repeat(auto-fit,minmax(min(100%,10rem),1fr))] gap-3">
<div className="app-surface-muted rounded-xl p-3">
<p className="app-text-muted text-xs">Период</p>
<p className="mt-1 text-sm font-medium leading-5 text-[var(--foreground)]">{getPayrollPeriodRange(selectedPeriod)}</p>
{periodStatus ? <span className={`app-status-pill mt-2 shrink-0 whitespace-nowrap ${periodStatus.className}`}>{periodStatus.label}</span> : null}
</div>
<div className="app-surface-muted rounded-xl p-3">
<p className="app-text-muted text-xs">Норма за период</p>
<p className="mt-1 text-lg font-semibold text-[var(--foreground)]">{formatPayrollPoints(summary?.target_points, "0")}</p>
<p className="app-text-muted mt-1 text-[11px]">
{summary?.target_source === "saved_record" ? "Из сохранённой записи" : `По графику: ${summary?.workdays_count || 0} дн.`}
</p>
</div>
<div className="app-surface-muted rounded-xl p-3">
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0 grow basis-24">
<p className="app-text-muted text-xs">Фактически за период</p>
<p className="mt-1 text-lg font-semibold text-[var(--foreground)]">
{formatPayrollPoints(summary?.actual_points, "0")} / {formatPayrollPoints(summary?.target_points, "0")}
</p>
</div>
{aggregateStatus ? <span className={`app-status-pill shrink-0 whitespace-nowrap ${aggregateStatus.className}`}>{aggregateStatus.label}</span> : null}
</div>
</div>
</div>
) : null}
</section>
<section className={cardClassName}>
<div className="mb-4 flex items-center justify-between gap-3">
<h2 className="text-base font-semibold text-[var(--foreground)]">Записи за период</h2>
<span className="app-counter h-6 min-w-6 px-1.5 text-xs">{workspace.entries.length}</span>
</div>
{workspace.entries.length ? (
<div className="space-y-2">
{workspace.entries.map((entry) => (
<button
type="button"
key={entry.id}
className={`app-surface-muted grid w-full grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-xl p-3 text-left transition hover:border-[var(--border-strong)] ${entry.work_date === workDate ? "app-selected" : ""}`}
onClick={() => selectWorkDate(entry.work_date)}
>
<span className="min-w-0">
<span className="block text-sm font-semibold text-[var(--foreground)]">{formatPayrollDate(entry.work_date)}</span>
<span className="app-text-muted mt-1 block truncate text-xs">{entry.note || "Без комментария"}</span>
</span>
<span className="flex shrink-0 items-center gap-3">
<span className="text-right">
<span className="block text-sm font-semibold text-[var(--foreground)]">{formatPayrollPoints(entry.actual_points, "0")}</span>
<span className="app-text-muted block text-[11px]">из {formatPayrollPoints(entry.target_points, "0")}</span>
</span>
{selectedPeriod?.editable ? <Pencil className="app-text-muted" size={15} /> : null}
</span>
</button>
))}
</div>
) : (
<div className="app-surface-muted rounded-xl px-4 py-8 text-center">
<p className="app-text-muted text-sm">За выбранный период записей пока нет.</p>
</div>
)}
</section>
{detailsTarget !== undefined
? detailsTarget ? createPortal(detailsContent, detailsTarget) : null
: detailsContent}
</div>
);
}

View File

@ -27,6 +27,7 @@ function compactDate(value: string): string {
}
export function PayrollWorkDatePicker({
compact = false,
periods,
selectedPeriod,
selectedDate,
@ -35,6 +36,7 @@ export function PayrollWorkDatePicker({
onSelectDate,
onSelectPeriod,
}: {
compact?: boolean;
periods: PayrollOwnWorkPeriod[];
selectedPeriod: PayrollOwnWorkPeriod | null;
selectedDate: string;
@ -78,10 +80,10 @@ export function PayrollWorkDatePicker({
}, [open]);
return (
<div className="relative w-44 max-w-full">
<div className={`relative max-w-full ${compact ? "w-40" : "w-44"}`}>
<button
type="button"
className="app-input flex h-11 w-full items-center gap-2 rounded-lg px-3 text-sm font-medium"
className={`${compact ? "app-action-secondary h-9 rounded-full text-xs" : "app-input h-11 rounded-lg text-sm"} flex w-full items-center gap-2 px-3 font-medium`}
disabled={disabled || !selectedPeriod}
aria-label="Дата выработки"
aria-expanded={open}

View File

@ -1,16 +1,20 @@
"use client";
import { ChevronDown, TrendingUp } from "lucide-react";
import { BarChart3, ChevronDown, TrendingUp } from "lucide-react";
import { useState } from "react";
import { PayrollDailyWorkSection } from "@/components/finance/PayrollDailyWorkSection";
import { Modal } from "@/components/ui/Modal";
export default function EmployeePayrollWorkProfileSection() {
const [open, setOpen] = useState(false);
const [statisticsOpen, setStatisticsOpen] = useState(false);
const [statisticsTarget, setStatisticsTarget] = useState<HTMLDivElement | null>(null);
const toggle = () => setOpen((current) => !current);
return (
<>
<section className="app-surface rounded-2xl">
<div className="flex min-w-0 items-center gap-2 px-4 py-4 sm:px-5">
<button
@ -27,6 +31,18 @@ export default function EmployeePayrollWorkProfileSection() {
</span>
</button>
<div id="profile-work-period-selector" className="shrink-0" />
<button
type="button"
onClick={() => setStatisticsOpen(true)}
className="app-action-ghost app-text-muted flex h-8 w-8 shrink-0 items-center justify-center rounded-lg transition hover:text-[var(--foreground)]"
title="Статистика по выработке"
aria-label="Открыть статистику по выработке"
>
<BarChart3 size={16} />
</button>
<button
type="button"
onClick={toggle}
@ -41,11 +57,25 @@ export default function EmployeePayrollWorkProfileSection() {
</button>
</div>
{open ? (
<div className="border-t border-[var(--border-subtle)] px-4 py-4 sm:px-5">
<PayrollDailyWorkSection embedded />
</div>
) : null}
<div className={open ? "border-t border-[var(--border-subtle)] px-4 py-4 sm:px-5" : "hidden"}>
<PayrollDailyWorkSection
compactPeriodSelector
detailsTarget={statisticsTarget}
embedded
periodSelectorTargetId="profile-work-period-selector"
/>
</div>
</section>
<Modal
isOpen={statisticsOpen}
onClose={() => setStatisticsOpen(false)}
title="Статистика по выработке"
size="md"
closeOnClickOutside
>
<div ref={setStatisticsTarget} />
</Modal>
</>
);
}