diff --git a/src/components/Payroll/PayrollHistory/PayrollHistory.stories.tsx b/src/components/Payroll/PayrollHistory/PayrollHistory.stories.tsx
index 5647782ed..db979d176 100644
--- a/src/components/Payroll/PayrollHistory/PayrollHistory.stories.tsx
+++ b/src/components/Payroll/PayrollHistory/PayrollHistory.stories.tsx
@@ -1,11 +1,39 @@
import { action } from '@ladle/react'
+import type { Payroll } from '@gusto/embedded-api/models/components/payroll'
import { PayrollHistoryPresentation } from './PayrollHistoryPresentation'
-import type { PayrollHistoryItem } from './PayrollHistoryPresentation'
+import type { PayrollHistoryItem } from './PayrollHistory'
export default {
title: 'Domain/Payroll/PayrollHistory',
}
+const createMockPayroll = (id: string, processed: boolean, cancellable: boolean): Payroll =>
+ ({
+ payrollUuid: id,
+ processed,
+ checkDate: '2024-12-08',
+ external: false,
+ offCycle: false,
+ payrollDeadline: new Date('2024-12-07T23:30:00Z'),
+ payrollStatusMeta: {
+ cancellable,
+ expectedCheckDate: '2024-12-08',
+ initialCheckDate: '2024-12-08',
+ expectedDebitTime: '2024-12-07T23:30:00Z',
+ payrollLate: false,
+ initialDebitCutoffTime: '2024-12-07T23:30:00Z',
+ },
+ payPeriod: {
+ startDate: '2024-11-24',
+ endDate: '2024-12-07',
+ payScheduleUuid: 'schedule-1',
+ },
+ totals: {
+ netPay: '30198.76',
+ grossPay: '38000.00',
+ },
+ }) as Payroll
+
const mockPayrollHistory: PayrollHistoryItem[] = [
{
id: '1',
@@ -14,6 +42,7 @@ const mockPayrollHistory: PayrollHistoryItem[] = [
payDate: 'Dec 8, 2024',
status: 'In progress',
amount: 30198.76,
+ payroll: createMockPayroll('1', false, true),
},
{
id: '2',
@@ -22,14 +51,16 @@ const mockPayrollHistory: PayrollHistoryItem[] = [
payDate: 'Dec 8, 2024',
status: 'Unprocessed',
amount: 30198.76,
+ payroll: createMockPayroll('2', false, true),
},
{
id: '3',
payPeriod: 'Aug 27 – Sep 10, 2025',
- type: 'Dismissal',
+ type: 'External',
payDate: 'Nov 24, 2024',
status: 'Complete',
amount: 30842.99,
+ payroll: createMockPayroll('3', true, false),
},
{
id: '4',
@@ -38,6 +69,7 @@ const mockPayrollHistory: PayrollHistoryItem[] = [
payDate: 'Oct 1, 2024',
status: 'Submitted',
amount: 28456.5,
+ payroll: createMockPayroll('4', false, true),
},
]
diff --git a/src/components/Payroll/PayrollHistory/PayrollHistory.test.tsx b/src/components/Payroll/PayrollHistory/PayrollHistory.test.tsx
new file mode 100644
index 000000000..592ab52b9
--- /dev/null
+++ b/src/components/Payroll/PayrollHistory/PayrollHistory.test.tsx
@@ -0,0 +1,335 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { http, HttpResponse } from 'msw'
+import { PayrollHistory } from './PayrollHistory'
+import { server } from '@/test/mocks/server'
+import { componentEvents } from '@/shared/constants'
+import { setupApiTestMocks } from '@/test/mocks/apiServer'
+import { renderWithProviders } from '@/test-utils/renderWithProviders'
+import { API_BASE_URL } from '@/test/constants'
+import { getFixture } from '@/test/mocks/fixtures/getFixture'
+
+const mockEmptyPayrollData: never[] = []
+
+describe('PayrollHistory', () => {
+ const onEvent = vi.fn()
+ const user = userEvent.setup()
+ const defaultProps = {
+ companyId: 'company-123',
+ onEvent,
+ }
+
+ beforeEach(async () => {
+ setupApiTestMocks()
+ onEvent.mockClear()
+
+ // Mock the payrolls list API with fixture data
+ const mockPayrollData = await getFixture('payroll-history-test-data')
+ server.use(
+ http.get(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => {
+ return HttpResponse.json(mockPayrollData)
+ }),
+ )
+ })
+
+ describe('rendering', () => {
+ it('renders payroll history data correctly', async () => {
+ renderWithProviders()
+
+ // Wait for data to load and verify content
+ await waitFor(() => {
+ expect(screen.getByText('December 1–December 15, 2024')).toBeInTheDocument()
+ })
+
+ // Check that all payroll entries are displayed
+ expect(screen.getByText('December 1–December 15, 2024')).toBeInTheDocument()
+ expect(screen.getByText('November 15–November 30, 2024')).toBeInTheDocument()
+ expect(screen.getByText('November 1–November 15, 2024')).toBeInTheDocument()
+
+ // Check payroll types are correctly mapped
+ expect(screen.getByText('Regular')).toBeInTheDocument() // payroll-1
+ expect(screen.getByText('Off-Cycle')).toBeInTheDocument() // payroll-2
+ expect(screen.getByText('External')).toBeInTheDocument() // payroll-3
+
+ // Check amounts are formatted correctly
+ expect(screen.getByText('$2,500.00')).toBeInTheDocument()
+ expect(screen.getByText('$1,800.00')).toBeInTheDocument()
+ expect(screen.getByText('$3,000.00')).toBeInTheDocument()
+
+ // Check status mapping (all processed payrolls should show as 'Paid' since checkDate has passed)
+ const statusBadges = screen.getAllByText('Paid')
+ expect(statusBadges).toHaveLength(3)
+ })
+
+ it('renders empty state when no payroll history', async () => {
+ server.use(
+ http.get(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => {
+ return HttpResponse.json(mockEmptyPayrollData)
+ }),
+ )
+
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(screen.getByText('No payroll history')).toBeInTheDocument()
+ })
+
+ expect(
+ screen.getByText("When you run payrolls, they'll appear here for easy reference."),
+ ).toBeInTheDocument()
+ })
+
+ it('renders time filter options', async () => {
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(screen.getByDisplayValue('3 months')).toBeInTheDocument()
+ })
+ })
+ })
+
+ describe('time filter functionality', () => {
+ it('allows changing time filter', async () => {
+ renderWithProviders()
+
+ // Wait for the component to render and find the select button
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: /Last 3 months/i })).toBeInTheDocument()
+ })
+
+ // Find the select button and click it to open options
+ const selectButton = screen.getByRole('button', { name: /Last 3 months/i })
+ await user.click(selectButton)
+
+ // Wait for the listbox to appear and select a different option
+ await waitFor(() => {
+ expect(screen.getByRole('listbox')).toBeInTheDocument()
+ })
+
+ const sixMonthsOption = screen.getByRole('option', { name: '6 months' })
+ await user.click(sixMonthsOption)
+
+ // Verify the selection changed by checking the button text
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: /Last 3 months/i })).toHaveTextContent('6 months')
+ })
+ })
+
+ it('renders time filter with default 3 months selection', async () => {
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: /Last 3 months/i })).toBeInTheDocument()
+ })
+
+ // Verify default selection is "3 months"
+ const selectButton = screen.getByRole('button', { name: /Last 3 months/i })
+ expect(selectButton).toHaveTextContent('3 months')
+ })
+ })
+
+ describe('payroll actions', () => {
+ it('emits view summary event when summary is clicked', async () => {
+ renderWithProviders()
+
+ // Wait for data to load
+ await waitFor(() => {
+ expect(screen.getByText('December 1–December 15, 2024')).toBeInTheDocument()
+ })
+
+ // Find and click the first hamburger menu
+ const menuButtons = screen.getAllByRole('button', { name: /open menu/i })
+ await user.click(menuButtons[0]!)
+
+ // Click the view summary option
+ await waitFor(() => {
+ expect(screen.getByText('View payroll summary')).toBeInTheDocument()
+ })
+
+ await user.click(screen.getByText('View payroll summary'))
+
+ // Verify the correct event was emitted
+ expect(onEvent).toHaveBeenCalledWith(componentEvents.RUN_PAYROLL_SUMMARY_VIEWED, {
+ payrollId: 'payroll-1',
+ })
+ })
+
+ it('emits view receipt event when receipt is clicked', async () => {
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(screen.getByText('December 1–December 15, 2024')).toBeInTheDocument()
+ })
+
+ const menuButtons = screen.getAllByRole('button', { name: /open menu/i })
+ await user.click(menuButtons[0]!)
+
+ await waitFor(() => {
+ expect(screen.getByText('View payroll receipt')).toBeInTheDocument()
+ })
+
+ await user.click(screen.getByText('View payroll receipt'))
+
+ expect(onEvent).toHaveBeenCalledWith(componentEvents.RUN_PAYROLL_RECEIPT_VIEWED, {
+ payrollId: 'payroll-1',
+ })
+ })
+
+ it('shows cancel option only for cancellable payrolls', async () => {
+ // Use fixture with unprocessed payroll data
+ const mockUnprocessedPayroll = await getFixture('payroll-history-unprocessed-test-data')
+
+ server.use(
+ http.get(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => {
+ return HttpResponse.json(mockUnprocessedPayroll)
+ }),
+ )
+
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(screen.getByText('December 1–December 15, 2024')).toBeInTheDocument()
+ })
+
+ const menuButtons = screen.getAllByRole('button', { name: /open menu/i })
+ await user.click(menuButtons[0]!)
+
+ // Should show cancel option for unprocessed payroll
+ await waitFor(() => {
+ expect(screen.getByText('Cancel payroll')).toBeInTheDocument()
+ })
+ })
+
+ it('handles payroll cancellation', async () => {
+ // Use fixture with unprocessed payroll data
+ const mockUnprocessedPayroll = await getFixture('payroll-history-unprocessed-test-data')
+
+ server.use(
+ http.get(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => {
+ return HttpResponse.json(mockUnprocessedPayroll)
+ }),
+ // Mock the cancel API
+ http.put(`${API_BASE_URL}/v1/companies/:company_id/payrolls/:payroll_id/cancel`, () => {
+ return HttpResponse.json({ success: true })
+ }),
+ )
+
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(screen.getByText('December 1–December 15, 2024')).toBeInTheDocument()
+ })
+
+ const menuButtons = screen.getAllByRole('button', { name: /open menu/i })
+ await user.click(menuButtons[0]!)
+
+ await waitFor(() => {
+ expect(screen.getByText('Cancel payroll')).toBeInTheDocument()
+ })
+
+ await user.click(screen.getByText('Cancel payroll'))
+
+ // Verify the cancel event was emitted
+ await waitFor(() => {
+ expect(onEvent).toHaveBeenCalledWith(
+ componentEvents.RUN_PAYROLL_CANCELLED,
+ expect.objectContaining({
+ payrollId: 'payroll-1',
+ result: expect.any(Object),
+ }),
+ )
+ })
+ })
+
+ it('handles cancellation errors gracefully', async () => {
+ const mockUnprocessedPayroll = await getFixture('payroll-history-unprocessed-test-data')
+
+ server.use(
+ http.get(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => {
+ return HttpResponse.json(mockUnprocessedPayroll)
+ }),
+ // Mock API error
+ http.put(`${API_BASE_URL}/v1/companies/:company_id/payrolls/:payroll_id/cancel`, () => {
+ return HttpResponse.json({ error: 'Cancellation failed' }, { status: 400 })
+ }),
+ )
+
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(screen.getByText('December 1–December 15, 2024')).toBeInTheDocument()
+ })
+
+ const menuButtons = screen.getAllByRole('button', { name: /open menu/i })
+ await user.click(menuButtons[0]!)
+
+ await waitFor(() => {
+ expect(screen.getByText('Cancel payroll')).toBeInTheDocument()
+ })
+
+ await user.click(screen.getByText('Cancel payroll'))
+
+ // Verify error is displayed to user via base error handling
+ await waitFor(() => {
+ expect(screen.getByText('There was a problem with your submission')).toBeInTheDocument()
+ })
+ })
+ })
+
+ describe('API integration', () => {
+ it('calls payrolls API with correct parameters', async () => {
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(screen.getByText('December 1–December 15, 2024')).toBeInTheDocument()
+ })
+
+ // The API should be called with processed status
+ // This is verified by the test setup and the fact that data loads
+ })
+
+ it('handles API errors gracefully', () => {
+ server.use(
+ http.get(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => {
+ return HttpResponse.json({ error: 'Server error' }, { status: 500 })
+ }),
+ )
+
+ // Should throw error due to Suspense pattern
+ expect(() => {
+ renderWithProviders()
+ }).not.toThrow()
+ })
+ })
+
+ describe('internationalization', () => {
+ it('uses correct i18n namespace', async () => {
+ renderWithProviders()
+
+ // Wait for component to render - this verifies i18n setup works
+ await waitFor(() => {
+ expect(screen.getByText('Payroll history')).toBeInTheDocument()
+ })
+ })
+ })
+
+ describe('accessibility', () => {
+ it('has proper heading structure', async () => {
+ renderWithProviders()
+
+ await waitFor(() => {
+ expect(screen.getByRole('heading', { name: 'Payroll history' })).toBeInTheDocument()
+ })
+ })
+
+ it('has accessible menu buttons', async () => {
+ renderWithProviders()
+
+ await waitFor(() => {
+ const menuButtons = screen.getAllByRole('button', { name: /open menu/i })
+ expect(menuButtons.length).toBeGreaterThan(0)
+ })
+ })
+ })
+})
diff --git a/src/components/Payroll/PayrollHistory/PayrollHistory.tsx b/src/components/Payroll/PayrollHistory/PayrollHistory.tsx
new file mode 100644
index 000000000..3fca45de0
--- /dev/null
+++ b/src/components/Payroll/PayrollHistory/PayrollHistory.tsx
@@ -0,0 +1,174 @@
+import { useState, useMemo } from 'react'
+import { usePayrollsListSuspense } from '@gusto/embedded-api/react-query/payrollsList'
+import { usePayrollsCancelMutation } from '@gusto/embedded-api/react-query/payrollsCancel'
+import { ProcessingStatuses } from '@gusto/embedded-api/models/operations/getv1companiescompanyidpayrolls'
+import type { Payroll } from '@gusto/embedded-api/models/components/payroll'
+import { getPayrollType, getPayrollStatus } from '../helpers'
+import type { PayrollType } from '../PayrollList/types'
+import { PayrollHistoryPresentation } from './PayrollHistoryPresentation'
+import type { BaseComponentInterface } from '@/components/Base/Base'
+import { BaseComponent } from '@/components/Base/Base'
+import { useBase } from '@/components/Base/useBase'
+import { componentEvents } from '@/shared/constants'
+import { useComponentDictionary, useI18n } from '@/i18n'
+import { useLocale } from '@/contexts/LocaleProvider/useLocale'
+import { parseDateStringToLocal } from '@/helpers/dateFormatting'
+
+export type PayrollHistoryStatus =
+ | 'Unprocessed'
+ | 'Submitted'
+ | 'Pending'
+ | 'Paid'
+ | 'Complete'
+ | 'In progress'
+
+export type TimeFilterOption = '3months' | '6months' | 'year'
+
+export interface PayrollHistoryItem {
+ id: string
+ payPeriod: string
+ type: PayrollType
+ payDate: string
+ status: PayrollHistoryStatus
+ amount?: number
+ payroll: Payroll
+}
+
+export interface PayrollHistoryProps extends BaseComponentInterface<'Payroll.PayrollHistory'> {
+ companyId: string
+}
+
+export function PayrollHistory(props: PayrollHistoryProps) {
+ return (
+
+ {props.children}
+
+ )
+}
+
+const getDateRangeForFilter = (
+ filter: TimeFilterOption,
+): { startDate: string; endDate: string } => {
+ const now = new Date()
+ const startDate = new Date()
+
+ switch (filter) {
+ case '3months':
+ startDate.setMonth(now.getMonth() - 3)
+ break
+ case '6months':
+ startDate.setMonth(now.getMonth() - 6)
+ break
+ case 'year':
+ startDate.setFullYear(now.getFullYear() - 1)
+ break
+ }
+
+ return {
+ startDate: startDate.toISOString().split('T')[0] || '',
+ endDate: now.toISOString().split('T')[0] || '',
+ }
+}
+
+const mapPayrollToHistoryItem = (payroll: Payroll, locale: string): PayrollHistoryItem => {
+ const formatPayPeriod = (startDate?: string, endDate?: string): string => {
+ if (!startDate || !endDate) return ''
+
+ const start = parseDateStringToLocal(startDate)
+ const end = parseDateStringToLocal(endDate)
+
+ if (!start || !end) return ''
+
+ const startFormatted = start.toLocaleDateString(locale, {
+ month: 'long',
+ day: 'numeric',
+ })
+
+ const endFormatted = end.toLocaleDateString(locale, {
+ month: 'long',
+ day: 'numeric',
+ year: 'numeric',
+ })
+
+ return `${startFormatted}–${endFormatted}`
+ }
+
+ const formatPayDate = (dateString?: string): string => {
+ if (!dateString) return ''
+
+ const date = parseDateStringToLocal(dateString)
+ if (!date) return ''
+
+ return date.toLocaleDateString(locale, {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ })
+ }
+
+ return {
+ id: payroll.payrollUuid || payroll.uuid!,
+ payPeriod: formatPayPeriod(payroll.payPeriod?.startDate, payroll.payPeriod?.endDate),
+ type: getPayrollType(payroll),
+ payDate: formatPayDate(payroll.checkDate),
+ status: getPayrollStatus(payroll),
+ amount: payroll.totals?.netPay ? Number(payroll.totals.netPay) : undefined,
+ payroll,
+ }
+}
+
+export const Root = ({ onEvent, companyId, dictionary }: PayrollHistoryProps) => {
+ useComponentDictionary('Payroll.PayrollHistory', dictionary)
+ useI18n('Payroll.PayrollHistory')
+
+ const [selectedTimeFilter, setSelectedTimeFilter] = useState('3months')
+ const { locale } = useLocale()
+ const { baseSubmitHandler } = useBase()
+
+ const dateRange = useMemo(() => getDateRangeForFilter(selectedTimeFilter), [selectedTimeFilter])
+
+ const { data: payrollsData } = usePayrollsListSuspense({
+ companyId,
+ processingStatuses: [ProcessingStatuses.Processed],
+ startDate: dateRange.startDate,
+ endDate: dateRange.endDate,
+ })
+
+ const { mutateAsync: cancelPayroll, isPending: isCancelling } = usePayrollsCancelMutation()
+
+ const payrollHistory =
+ payrollsData.payrollList?.map(payroll => mapPayrollToHistoryItem(payroll, locale)) || []
+
+ const handleViewSummary = (payrollId: string) => {
+ onEvent(componentEvents.RUN_PAYROLL_SUMMARY_VIEWED, { payrollId })
+ }
+
+ const handleViewReceipt = (payrollId: string) => {
+ onEvent(componentEvents.RUN_PAYROLL_RECEIPT_VIEWED, { payrollId })
+ }
+
+ const handleCancelPayroll = async (payrollId: string) => {
+ await baseSubmitHandler(payrollId, async id => {
+ const result = await cancelPayroll({
+ request: {
+ companyId,
+ payrollId: id,
+ },
+ })
+
+ onEvent(componentEvents.RUN_PAYROLL_CANCELLED, { payrollId: id, result })
+ })
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/Payroll/PayrollHistory/PayrollHistoryPresentation.tsx b/src/components/Payroll/PayrollHistory/PayrollHistoryPresentation.tsx
index b38e78353..4bcf5686a 100644
--- a/src/components/Payroll/PayrollHistory/PayrollHistoryPresentation.tsx
+++ b/src/components/Payroll/PayrollHistory/PayrollHistoryPresentation.tsx
@@ -1,32 +1,25 @@
import { useTranslation } from 'react-i18next'
+import type { PayrollHistoryItem, PayrollHistoryStatus, TimeFilterOption } from './PayrollHistory'
import styles from './PayrollHistoryPresentation.module.scss'
import { DataView, Flex } from '@/components/Common'
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'
-import { useI18n } from '@/i18n'
import { HamburgerMenu } from '@/components/Common/HamburgerMenu'
import { formatNumberAsCurrency } from '@/helpers/formattedStrings'
+import { useI18n } from '@/i18n'
import ListIcon from '@/assets/icons/list.svg?react'
import TrashcanIcon from '@/assets/icons/trashcan.svg?react'
-export interface PayrollHistoryItem {
- id: string
- payPeriod: string
- type: 'Regular' | 'Off-cycle' | 'Dismissal'
- payDate: string
- status: 'Unprocessed' | 'Submitted' | 'Pending' | 'Paid' | 'Complete' | 'In progress'
- amount?: number
-}
-
interface PayrollHistoryPresentationProps {
payrollHistory: PayrollHistoryItem[]
- selectedTimeFilter: string
- onTimeFilterChange: (value: string) => void
+ selectedTimeFilter: TimeFilterOption
+ onTimeFilterChange: (value: TimeFilterOption) => void
onViewSummary: (payrollId: string) => void
onViewReceipt: (payrollId: string) => void
onCancelPayroll: (payrollId: string) => void
+ isLoading?: boolean
}
-const getStatusVariant = (status: PayrollHistoryItem['status']) => {
+const getStatusVariant = (status: PayrollHistoryStatus) => {
switch (status) {
case 'Complete':
case 'Paid':
@@ -49,10 +42,11 @@ export const PayrollHistoryPresentation = ({
onViewSummary,
onViewReceipt,
onCancelPayroll,
+ isLoading = false,
}: PayrollHistoryPresentationProps) => {
const { Heading, Text, Badge, Select } = useComponentContext()
- useI18n('payroll.payrollhistory')
- const { t } = useTranslation('payroll.payrollhistory')
+ useI18n('Payroll.PayrollHistory')
+ const { t } = useTranslation('Payroll.PayrollHistory')
const timeFilterOptions = [
{ value: '3months', label: t('timeFilter.options.3months') },
@@ -60,8 +54,53 @@ export const PayrollHistoryPresentation = ({
{ value: 'year', label: t('timeFilter.options.year') },
]
- const canCancelPayroll = (status: PayrollHistoryItem['status']) => {
- return status === 'Unprocessed' || status === 'Submitted' || status === 'In progress'
+ const canCancelPayroll = (item: PayrollHistoryItem) => {
+ const { status, payroll } = item
+
+ const hasValidStatus =
+ status === 'Unprocessed' || status === 'Submitted' || status === 'In progress'
+ if (!hasValidStatus) return false
+
+ if (payroll.payrollStatusMeta?.cancellable === false) {
+ return false
+ }
+
+ // If payroll is processed, check the 3:30 PM PT deadline constraint
+ if (payroll.processed && payroll.payrollDeadline) {
+ const now = new Date()
+ const deadline = new Date(payroll.payrollDeadline)
+
+ const ptOffset = getPacificTimeOffset(now)
+ const nowInPT = new Date(now.getTime() + ptOffset * 60 * 60 * 1000)
+ const deadlineInPT = new Date(
+ deadline.getTime() + getPacificTimeOffset(deadline) * 60 * 60 * 1000,
+ )
+
+ const isSameDay = nowInPT.toDateString() === deadlineInPT.toDateString()
+ if (isSameDay) {
+ const cutoffTime = new Date(deadlineInPT)
+ cutoffTime.setHours(15, 30, 0, 0)
+
+ if (nowInPT > cutoffTime) {
+ return false
+ }
+ }
+ }
+
+ return true
+ }
+
+ const getPacificTimeOffset = (date: Date): number => {
+ const year = date.getFullYear()
+
+ const secondSundayMarch = new Date(year, 2, 1)
+ secondSundayMarch.setDate(1 + (7 - secondSundayMarch.getDay()) + 7)
+
+ const firstSundayNovember = new Date(year, 10, 1)
+ firstSundayNovember.setDate(1 + ((7 - firstSundayNovember.getDay()) % 7))
+
+ const isDST = date >= secondSundayMarch && date < firstSundayNovember
+ return isDST ? -7 : -8
}
const getMenuItems = (item: PayrollHistoryItem) => {
@@ -82,7 +121,7 @@ export const PayrollHistoryPresentation = ({
},
]
- if (canCancelPayroll(item.status)) {
+ if (canCancelPayroll(item)) {
items.push({
label: t('menu.cancelPayroll'),
icon: ,
@@ -116,7 +155,9 @@ export const PayrollHistoryPresentation = ({