Refine procurement date badges and profile attendance visibility

This commit is contained in:
lizerk 2026-06-24 12:31:46 +03:00
parent ffb101d984
commit 14b42baa2c
4 changed files with 61 additions and 12 deletions

View File

@ -40,6 +40,7 @@ import { SearchableSelectSingle } from "@/components/shared/SearchableSelect";
import { formatDate, formatDateTime, formatMoney } from "@/lib/shared";
import { useProcurementPage } from "@/hooks/useProcurementPage";
import { Modal } from "@/components/ui";
import { getExpectedDeliveryDateBadgeClass } from "@/lib/procurementDates";
import { getProcurementQuantityUnitLabel, getProcurementUnitOptions } from "@/lib/procurementUnits";
/* ══════════════════════════════════════════════════════
@ -859,6 +860,7 @@ export default function ProcurementPage() {
const totalRequestedQuantity = resolvedDetail.total_requested_quantity ?? req.total_requested_quantity ?? detailItems.reduce((sum, item) => sum + Number(item.quantity || 0), 0);
const totalOrderedQuantity = resolvedDetail.total_ordered_quantity ?? req.total_ordered_quantity ?? detailItems.reduce((sum, item) => sum + Number(item.ordered_quantity || 0), 0);
const totalReceivedQuantity = resolvedDetail.total_received_quantity ?? req.total_received_quantity ?? detailItems.reduce((sum, item) => sum + Number(item.received_quantity || 0), 0);
const isRequestFullyReceived = totalRequestedQuantity > 0 && totalReceivedQuantity >= totalRequestedQuantity;
const quantityUnitLabel = getProcurementQuantityUnitLabel(
detailItems,
resolvedDetail.quantity_unit_label ?? req.quantity_unit_label,
@ -1040,7 +1042,7 @@ export default function ProcurementPage() {
</span>
) : null}
{nextExpectedDeliveryDate ? (
<span className="app-badge inline-flex items-center gap-1 rounded-full px-2 py-1 text-[11px] font-medium">
<span className={`${getExpectedDeliveryDateBadgeClass(nextExpectedDeliveryDate, isRequestFullyReceived)} inline-flex items-center gap-1 rounded-full px-2 py-1 text-[11px] font-medium`}>
<CalendarDays size={11} /> {fmt(nextExpectedDeliveryDate)}
</span>
) : null}

View File

@ -108,6 +108,15 @@ export default function UserDetailPage() {
() => getProfileDepartmentSummary(person?.departments),
[person?.departments],
);
const canViewAttendance = Boolean(
currentUser
&& person
&& (
currentUser.id === person.id
|| currentUser.auth?.is_staff
|| currentUser.auth?.is_superuser
),
);
const [availableSkills, setAvailableSkills] = useState<
Array<{ id: number; name: string; description?: string }>
>([]);
@ -527,17 +536,21 @@ export default function UserDetailPage() {
expandedCommentLength={240}
/>
<ProfileWorkScheduleCard
canEdit={Boolean(currentUser?.auth?.is_staff || currentUser?.auth?.is_superuser)}
employeeId={person.id}
personnelState={person.personnel_state}
/>
{canViewAttendance ? (
<>
<ProfileWorkScheduleCard
canEdit={Boolean(currentUser?.auth?.is_staff || currentUser?.auth?.is_superuser)}
employeeId={person.id}
personnelState={person.personnel_state}
/>
<EmployeeAttendanceCard
attendanceAliases={person.attendance_aliases}
employeeActions={person.actions}
employeeId={person.id}
/>
<EmployeeAttendanceCard
attendanceAliases={person.attendance_aliases}
employeeActions={person.actions}
employeeId={person.id}
/>
</>
) : null}
</>
) : null}
</div>

View File

@ -8,6 +8,7 @@ import { CommentComposer, CommentDeleteButton } from "@/components/shared/Commen
import { Modal } from "@/components/ui";
import { cleanLinkRows, linkHref, toLinkRows } from "@/lib/procurementLinks";
import { getExpectedDeliveryDateBadgeClass } from "@/lib/procurementDates";
import { getProcurementUnitLabel } from "@/lib/procurementUnits";
import { formatDate, formatMoney, userProfileLink } from "@/lib/shared";
import type { ProcurementItem, ProcurementItemComment, ProcurementItemExecutionStatus, ProcurementRequest, User } from "@/types/api";
@ -220,6 +221,7 @@ function ProcurementItemCard({
const itemUnitLabel = getProcurementUnitLabel(item.unit);
const orderedQuantityMarkerClass = quantityProgressMarkerClass(orderedQuantity, requestedQuantity);
const receivedQuantityMarkerClass = quantityProgressMarkerClass(receivedQuantity, requestedQuantity);
const isItemFullyReceived = requestedQuantity > 0 && receivedQuantity >= requestedQuantity;
const status = item.execution_status || "pending";
const statusLabel = item.execution_status_display || executionStatusOptions.find((option) => option.value === status)?.label || "Не выполнено";
const canCancelReceived = Boolean(
@ -376,7 +378,7 @@ function ProcurementItemCard({
{expectedDeliveryDates.length > 0 ? (
<div className="mt-1 flex flex-wrap gap-1">
{expectedDeliveryDates.map((date, dateIndex) => (
<span key={`${item.id}-date-${dateIndex}`} className="app-badge px-2 py-0.5 text-[11px] font-medium">
<span key={`${item.id}-date-${dateIndex}`} className={`${getExpectedDeliveryDateBadgeClass(date, isItemFullyReceived)} rounded-full px-2 py-0.5 text-[11px] font-medium`}>
{formatDate(date)}
</span>
))}

View File

@ -0,0 +1,32 @@
const toDateOnlyTime = (value?: string | null): number | null => {
if (!value) return null;
const match = String(value).match(/^(\d{4})-(\d{2})-(\d{2})/);
if (!match) return null;
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return null;
const date = new Date(year, month - 1, day);
date.setHours(0, 0, 0, 0);
return date.getTime();
};
export const getExpectedDeliveryDateBadgeClass = (
value?: string | null,
isFullyReceived = false,
): string => {
if (isFullyReceived) return "app-badge";
const dateTime = toDateOnlyTime(value);
if (dateTime === null) return "app-badge";
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayTime = today.getTime();
if (dateTime < todayTime) return "app-feedback-danger";
if (dateTime === todayTime) return "app-feedback-warning";
return "app-badge";
};