Merge branch 'develop'
This commit is contained in:
commit
411eda9765
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user