diff --git a/frontend/src/app/procurement/page.tsx b/frontend/src/app/procurement/page.tsx index 06c89b80..83f6c55e 100644 --- a/frontend/src/app/procurement/page.tsx +++ b/frontend/src/app/procurement/page.tsx @@ -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() { ) : null} {nextExpectedDeliveryDate ? ( - + {fmt(nextExpectedDeliveryDate)} ) : null} diff --git a/frontend/src/app/users/[id]/page.tsx b/frontend/src/app/users/[id]/page.tsx index e5479b28..929d302b 100644 --- a/frontend/src/app/users/[id]/page.tsx +++ b/frontend/src/app/users/[id]/page.tsx @@ -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} /> - + {canViewAttendance ? ( + <> + - + + + ) : null} ) : null} diff --git a/frontend/src/components/procurement/ProcurementRequestDetailContent.tsx b/frontend/src/components/procurement/ProcurementRequestDetailContent.tsx index e851a212..8492f6d7 100644 --- a/frontend/src/components/procurement/ProcurementRequestDetailContent.tsx +++ b/frontend/src/components/procurement/ProcurementRequestDetailContent.tsx @@ -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 ? (
{expectedDeliveryDates.map((date, dateIndex) => ( - + {formatDate(date)} ))} diff --git a/frontend/src/lib/procurementDates.ts b/frontend/src/lib/procurementDates.ts new file mode 100644 index 00000000..291b3e07 --- /dev/null +++ b/frontend/src/lib/procurementDates.ts @@ -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"; +};