@ossy/timesheets 3.8.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,20 +1,23 @@
1
1
  {
2
2
  "name": "@ossy/timesheets",
3
3
  "description": "Timesheets — generate, review, save, and export monthly work hours",
4
- "version": "3.8.0",
4
+ "version": "3.9.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "module": "./src/index.js",
8
8
  "exports": {
9
9
  ".": "./src/index.js"
10
10
  },
11
+ "scripts": {
12
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose"
13
+ },
11
14
  "author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
12
15
  "license": "MIT",
13
16
  "ossy": {
14
17
  "src": "./src"
15
18
  },
16
19
  "dependencies": {
17
- "@ossy/resources": "^3.8.0",
20
+ "@ossy/resources": "^3.9.0",
18
21
  "html-to-image": "^1.11.13",
19
22
  "nanoid": "^5.1.11",
20
23
  "pdf-lib": "^1.17.1"
@@ -38,5 +41,5 @@
38
41
  "/src",
39
42
  "README.md"
40
43
  ],
41
- "gitHead": "14548dfc2019e7258e8c75db527acbf66fe829bd"
44
+ "gitHead": "f404be69becb27a1fd853a6ff1903554e76e7d17"
42
45
  }
package/src/Timesheet.jsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import React, { useMemo } from 'react'
2
2
  import { View, Text, Input, DateDisplay, Separator, useLocale } from '@ossy/design-system'
3
3
  import { buildMonthGrid, dateKeyFromMs } from '@ossy/calendar'
4
+ import { TimesheetPartyRows } from './TimesheetPartyRows.jsx'
4
5
 
5
6
  const calendarGrid = {
6
7
  display: 'grid',
@@ -53,6 +54,8 @@ const hoursInput = {
53
54
  borderStyle: 'solid',
54
55
  boxShadow: 'none',
55
56
  outline: 'none',
57
+ appearance: 'textfield',
58
+ MozAppearance: 'textfield',
56
59
  }
57
60
 
58
61
  const WEEKDAY_KEYS = [
@@ -70,7 +73,17 @@ const WEEKDAY_KEYS = [
70
73
  * Non-workdays use CalendarDaySurfaceStyles (red).
71
74
  * Pass `captureRef` only if capturing this live card; prefer {@link TimesheetExportCard} for PNG/PDF.
72
75
  */
73
- export function Timesheet ({ lines = [], onChange, disabled = false, periodStart, captureRef }) {
76
+ export function Timesheet ({
77
+ lines = [],
78
+ onChange,
79
+ disabled = false,
80
+ periodStart,
81
+ captureRef,
82
+ employeeId,
83
+ contractId,
84
+ onEmployeeChange,
85
+ onContractChange,
86
+ }) {
74
87
  const { t } = useLocale()
75
88
 
76
89
  const lineByKey = useMemo(() => {
@@ -139,6 +152,15 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
139
152
  font-weight: 700;
140
153
  font-size: 14px;
141
154
  text-align: center;
155
+ appearance: textfield;
156
+ -moz-appearance: textfield;
157
+ }
158
+
159
+ [data-timesheet-root] [data-ossy-calendar-day] [data-component="input"]::-webkit-outer-spin-button,
160
+ [data-timesheet-root] [data-ossy-calendar-day] [data-component="input"]::-webkit-inner-spin-button {
161
+ -webkit-appearance: none;
162
+ margin: 0;
163
+ display: none;
142
164
  }
143
165
 
144
166
  [data-timesheet-root] [data-ossy-calendar-day] [data-component="input"]:focus {
@@ -178,6 +200,19 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
178
200
  />
179
201
  </View>
180
202
 
203
+ {typeof onEmployeeChange === 'function' && typeof onContractChange === 'function' ? (
204
+ <div style={{ width: 0, minWidth: '100%' }}>
205
+ <TimesheetPartyRows
206
+ employeeId={employeeId}
207
+ contractId={contractId}
208
+ onEmployeeChange={onEmployeeChange}
209
+ onContractChange={onContractChange}
210
+ disabled={disabled}
211
+ totalHours={totalHours}
212
+ />
213
+ </div>
214
+ ) : null}
215
+
181
216
  <div style={calendarGrid}>
182
217
  {WEEKDAY_KEYS.map((key) => (
183
218
  <Text key={key} variant="small" color="secondary" style={{ textAlign: 'center' }}>
@@ -1,36 +1,75 @@
1
- import React, { useMemo } from 'react'
2
- import { View, Text, DateDisplay, List, Icon, useLocale } from '@ossy/design-system'
1
+ import React, { useCallback, useMemo, useState } from 'react'
2
+ import { Button, ContextMenu, DateDisplay, Dropdown, Icon, List, Text, View, useLocale } from '@ossy/design-system'
3
3
 
4
4
  /**
5
- * List row for a timesheet period.
5
+ * Storage-style list row for a timesheet period.
6
6
  */
7
- export function TimesheetCard ({ timesheet, onClick }) {
7
+ export function TimesheetCard ({ timesheet, onClick, selected = false, actions }) {
8
8
  const { t } = useLocale()
9
+ const [menuOpen, setMenuOpen] = useState(false)
10
+ const [menuPosition, setMenuPosition] = useState(null)
9
11
  const totalHours = useMemo(
10
12
  () => (timesheet?.lines ?? []).reduce((sum, line) => sum + (Number(line.hours) || 0), 0),
11
13
  [timesheet?.lines],
12
14
  )
13
15
  const status = timesheet?.status ?? 'draft'
14
16
 
17
+ const handleMenuOpenChange = useCallback((open) => {
18
+ setMenuOpen(open)
19
+ if (!open) setMenuPosition(null)
20
+ }, [])
21
+
22
+ const handleContextMenu = useCallback((event) => {
23
+ if (!actions) return
24
+ event.preventDefault()
25
+ event.stopPropagation()
26
+ setMenuPosition({ top: event.clientY, left: event.clientX })
27
+ setMenuOpen(true)
28
+ }, [actions])
29
+
15
30
  return (
16
31
  <List.Item
17
32
  selectable
18
- density="comfortable"
33
+ selected={selected}
19
34
  onClick={onClick}
20
- trailing={<Icon name="chevron-right" size="s" />}
21
- >
22
- <View gap="xxs" style={{ minWidth: 0 }}>
23
- <DateDisplay
24
- value={timesheet.periodStart}
25
- formatOptions={{ month: 'long', year: 'numeric' }}
26
- weight="medium"
27
- />
28
- <Text variant="small" color="secondary">
29
- {t('timesheets.statusLabel', { status: t(`timesheets.status.${status}`) })}
30
- {' · '}
31
- {t('timesheets.totalHours', { hours: totalHours })}
35
+ onContextMenu={handleContextMenu}
36
+ leading={(
37
+ <Icon size="s" name="calendar" style={{ fill: 'hsl(0, 0%, 80%)' }} />
38
+ )}
39
+ meta={(
40
+ <Text variant="small">
41
+ {t('timesheets.meta.hoursValue', { hours: totalHours })} {t(`timesheets.status.${status}`)}
32
42
  </Text>
33
- </View>
43
+ )}
44
+ trailing={actions ? (
45
+ <Dropdown
46
+ trigger={(
47
+ <Button
48
+ prefix="more-vertical-alt"
49
+ variant="command"
50
+ aria-label={t('timesheets.rowActions')}
51
+ onClick={(event) => {
52
+ event.stopPropagation()
53
+ setMenuPosition(null)
54
+ }}
55
+ />
56
+ )}
57
+ open={menuOpen}
58
+ onOpenChange={handleMenuOpenChange}
59
+ position={menuPosition}
60
+ >
61
+ <View inset="xs" surface="primary" roundness="s">
62
+ <ContextMenu roundness="s" surface="primary">
63
+ {actions}
64
+ </ContextMenu>
65
+ </View>
66
+ </Dropdown>
67
+ ) : undefined}
68
+ >
69
+ <DateDisplay
70
+ value={timesheet.periodStart}
71
+ formatOptions={{ month: 'long', year: 'numeric' }}
72
+ />
34
73
  </List.Item>
35
74
  )
36
75
  }
@@ -46,12 +46,42 @@ const WEEKDAY_KEYS = [
46
46
  'timesheets.weekday.sun',
47
47
  ]
48
48
 
49
+ function ExportMetaRow ({ label, value }) {
50
+ if (value == null || value === '') return null
51
+ return (
52
+ <div style={{ display: 'flex', justifyContent: 'space-between', gap: 16 }}>
53
+ <span style={{ fontFamily: TIMESHEET_EXPORT_FONT_FAMILY, fontSize: 13, fontWeight: 400, color: FG_MUTED }}>
54
+ {label}
55
+ </span>
56
+ <span style={{ fontFamily: TIMESHEET_EXPORT_FONT_FAMILY, fontSize: 13, fontWeight: 400, color: FG }}>
57
+ {value}
58
+ </span>
59
+ </div>
60
+ )
61
+ }
62
+
63
+ function formatSek (value, language) {
64
+ if (value == null || value === '' || !Number.isFinite(Number(value))) return ''
65
+ return new Intl.NumberFormat(language === 'sv' ? 'sv-SE' : 'en-SE', {
66
+ style: 'currency',
67
+ currency: 'SEK',
68
+ }).format(Number(value))
69
+ }
70
+
49
71
  /**
50
72
  * Print-ready timesheet (hours as text — no inputs).
51
73
  * Matches on-screen {@link Timesheet} layout; white background, no card chrome.
52
74
  * Mount off-screen and pass `captureRef` for PNG/PDF via html-to-image.
53
75
  */
54
- export function TimesheetExportCard ({ lines = [], periodStart, captureRef }) {
76
+ export function TimesheetExportCard ({
77
+ lines = [],
78
+ periodStart,
79
+ captureRef,
80
+ employeeLabel,
81
+ contractLabel,
82
+ clientName,
83
+ hourlyRate,
84
+ }) {
55
85
  const { t, language } = useLocale()
56
86
 
57
87
  const lineByKey = useMemo(() => {
@@ -74,6 +104,12 @@ export function TimesheetExportCard ({ lines = [], periodStart, captureRef }) {
74
104
  .format(new Date(anchorMs))
75
105
  }, [anchorMs, language])
76
106
 
107
+ const rate = hourlyRate == null || hourlyRate === '' ? null : Number(hourlyRate)
108
+ const totalCost = rate != null && Number.isFinite(rate) ? totalHours * rate : null
109
+ const hasPartyMeta = Boolean(
110
+ employeeLabel || contractLabel || clientName || (rate != null && Number.isFinite(rate)),
111
+ )
112
+
77
113
  if (!lines.length) return null
78
114
 
79
115
  return (
@@ -135,6 +171,25 @@ export function TimesheetExportCard ({ lines = [], periodStart, captureRef }) {
135
171
  width: '100%',
136
172
  }}
137
173
  />
174
+ {hasPartyMeta ? (
175
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
176
+ <ExportMetaRow label={t('timesheets.employee')} value={employeeLabel} />
177
+ <ExportMetaRow label={t('timesheets.meta.client')} value={clientName} />
178
+ <ExportMetaRow label={t('timesheets.contract')} value={contractLabel} />
179
+ <ExportMetaRow
180
+ label={t('timesheets.meta.hourlyRate')}
181
+ value={
182
+ rate != null && Number.isFinite(rate)
183
+ ? `${formatSek(rate, language)}${t('timesheets.meta.perHour')}`
184
+ : ''
185
+ }
186
+ />
187
+ <ExportMetaRow
188
+ label={t('timesheets.meta.totalCost')}
189
+ value={totalCost != null && Number.isFinite(totalCost) ? formatSek(totalCost, language) : ''}
190
+ />
191
+ </div>
192
+ ) : null}
138
193
  </div>
139
194
 
140
195
  <div style={calendarGrid}>
@@ -0,0 +1,249 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
2
+ import { Text, View, Button, useLocale, Alert, Dropdown, ContextMenu, DateDisplay } from '@ossy/design-system'
3
+ import { useSdk } from '@ossy/sdk-react'
4
+ import { Timesheet } from './Timesheet.jsx'
5
+ import { TimesheetExportCard } from './TimesheetExportCard.jsx'
6
+ import { downloadTimesheetCsv } from './download-timesheet-csv.js'
7
+ import { downloadTimesheetImage } from './download-timesheet-image.js'
8
+ import { downloadTimesheetPdf } from './download-timesheet-pdf.js'
9
+ import { invokeErrorMessage } from './invoke-error-message.js'
10
+ import { useTimesheetParties } from './TimesheetPartyFields.jsx'
11
+ import { contractBillingMeta, labelForContract, labelForEmployee } from './timesheet-parties.js'
12
+ import { metadata as GetTimesheet } from './get.action.js'
13
+ import { metadata as SaveTimesheet } from './save.action.js'
14
+
15
+ /**
16
+ * Storage-style side panel for reviewing and saving a timesheet.
17
+ */
18
+ export function TimesheetPanel ({ timesheetId, onClose, onSaved, width = '50%' }) {
19
+ const { t } = useLocale()
20
+ const sdk = useSdk()
21
+ const captureRef = useRef(null)
22
+ const { employees, contracts } = useTimesheetParties()
23
+
24
+ const [timesheet, setTimesheet] = useState(null)
25
+ const [lines, setLines] = useState([])
26
+ const [employeeId, setEmployeeId] = useState(null)
27
+ const [contractId, setContractId] = useState(null)
28
+ const [loading, setLoading] = useState(true)
29
+ const [saving, setSaving] = useState(false)
30
+ const [exporting, setExporting] = useState(false)
31
+ const [error, setError] = useState(null)
32
+ const [message, setMessage] = useState(null)
33
+
34
+ const load = useCallback(async () => {
35
+ if (!timesheetId) return
36
+ setLoading(true)
37
+ setError(null)
38
+ setMessage(null)
39
+ try {
40
+ const data = await sdk.invoke(GetTimesheet, { timesheetId })
41
+ if (!data) {
42
+ setError(t('timesheets.errorNotFound'))
43
+ setTimesheet(null)
44
+ setLines([])
45
+ setEmployeeId(null)
46
+ setContractId(null)
47
+ return
48
+ }
49
+ setTimesheet(data)
50
+ setLines(data.lines ?? [])
51
+ setEmployeeId(data.employeeId ?? null)
52
+ setContractId(data.contractId ?? null)
53
+ } catch (err) {
54
+ setError(await invokeErrorMessage(err, t('timesheets.errorLoad')))
55
+ setTimesheet(null)
56
+ setLines([])
57
+ setEmployeeId(null)
58
+ setContractId(null)
59
+ } finally {
60
+ setLoading(false)
61
+ }
62
+ }, [sdk, t, timesheetId])
63
+
64
+ useEffect(() => {
65
+ load()
66
+ }, [load])
67
+
68
+ const handleSave = async () => {
69
+ if (!timesheet?.id) return
70
+ setSaving(true)
71
+ setError(null)
72
+ setMessage(null)
73
+ try {
74
+ const data = await sdk.invoke(SaveTimesheet, {
75
+ timesheetId: timesheet.id,
76
+ lines,
77
+ employeeId,
78
+ contractId,
79
+ })
80
+ setTimesheet(data)
81
+ setLines(data?.lines ?? lines)
82
+ setEmployeeId(data?.employeeId ?? null)
83
+ setContractId(data?.contractId ?? null)
84
+ setMessage(t('timesheets.saved'))
85
+ onSaved?.(data)
86
+ } catch (err) {
87
+ setError(await invokeErrorMessage(err, t('timesheets.errorSave')))
88
+ } finally {
89
+ setSaving(false)
90
+ }
91
+ }
92
+
93
+ const billing = contractBillingMeta(contracts, contractId)
94
+
95
+ const exportPayload = {
96
+ ...timesheet,
97
+ lines,
98
+ status: timesheet?.status,
99
+ clientName: billing.clientName,
100
+ hourlyRate: billing.hourlyRate,
101
+ }
102
+
103
+ const runExport = async (kind) => {
104
+ if (!lines.length) return
105
+ setExporting(true)
106
+ setError(null)
107
+ setMessage(null)
108
+ try {
109
+ if (kind === 'csv') {
110
+ downloadTimesheetCsv(exportPayload)
111
+ } else if (kind === 'png') {
112
+ await downloadTimesheetImage(captureRef.current, exportPayload)
113
+ } else if (kind === 'pdf') {
114
+ await downloadTimesheetPdf(captureRef.current, exportPayload)
115
+ }
116
+ } catch (err) {
117
+ setError(await invokeErrorMessage(err, t('timesheets.errorExport')))
118
+ } finally {
119
+ setExporting(false)
120
+ }
121
+ }
122
+
123
+ if (!timesheetId) return null
124
+
125
+ const exportDisabled = !lines.length || loading || exporting
126
+
127
+ return (
128
+ <View
129
+ stack
130
+ surface="primary"
131
+ bordered
132
+ roundness="xs"
133
+ style={{ height: '100%', width, minHeight: 0, minWidth: 0, flexShrink: 0 }}
134
+ >
135
+ <View.Item>
136
+ <View stack horizontal style={{ minHeight: '48px', alignItems: 'center', gap: '4px' }}>
137
+ <View.Item fill surface="primary" style={{ padding: '4px 8px', minWidth: 0 }}>
138
+ {timesheet?.periodStart != null ? (
139
+ <DateDisplay
140
+ value={timesheet.periodStart}
141
+ formatOptions={{ month: 'long', year: 'numeric' }}
142
+ as="h3"
143
+ variant="small"
144
+ weight="medium"
145
+ />
146
+ ) : (
147
+ <Text as="h3" variant="small" weight="medium" text="timesheets.detail.title" />
148
+ )}
149
+ </View.Item>
150
+
151
+ <Dropdown
152
+ trigger={(
153
+ <Button
154
+ variant="command"
155
+ prefix="software-download"
156
+ disabled={exportDisabled}
157
+ aria-label={t('timesheets.export')}
158
+ />
159
+ )}
160
+ >
161
+ <ContextMenu roundness="s" surface="primary">
162
+ <ContextMenu.Item
163
+ prefix="list"
164
+ label="timesheets.exportCsv"
165
+ onClick={() => runExport('csv')}
166
+ />
167
+ <ContextMenu.Item
168
+ prefix="image"
169
+ label="timesheets.exportPng"
170
+ onClick={() => runExport('png')}
171
+ />
172
+ <ContextMenu.Item
173
+ prefix="file-document"
174
+ label="timesheets.exportPdf"
175
+ onClick={() => runExport('pdf')}
176
+ />
177
+ </ContextMenu>
178
+ </Dropdown>
179
+
180
+ <Button
181
+ id={SaveTimesheet.id}
182
+ variant="command"
183
+ prefix="check"
184
+ disabled={!timesheet?.id || saving || loading}
185
+ aria-label={saving ? t('timesheets.saving') : t('timesheets.save')}
186
+ onClick={handleSave}
187
+ />
188
+
189
+ {onClose && (
190
+ <Button
191
+ prefix="close"
192
+ variant="command"
193
+ aria-label={t('timesheets.closePanel')}
194
+ onClick={onClose}
195
+ />
196
+ )}
197
+ </View>
198
+ </View.Item>
199
+
200
+ <View.Item
201
+ fill
202
+ style={{ display: 'flex', flexDirection: 'column', minHeight: 0, overflowY: 'auto' }}
203
+ >
204
+ <View inset="m" gap="m">
205
+ {error && <Alert variant="danger">{error}</Alert>}
206
+ {message && !error && <Alert variant="success">{message}</Alert>}
207
+
208
+ {loading ? (
209
+ <Text color="secondary" text="timesheets.loading" />
210
+ ) : timesheet ? (
211
+ <>
212
+ <Timesheet
213
+ lines={lines}
214
+ onChange={setLines}
215
+ disabled={loading || saving || exporting}
216
+ periodStart={timesheet.periodStart}
217
+ employeeId={employeeId}
218
+ contractId={contractId}
219
+ onEmployeeChange={setEmployeeId}
220
+ onContractChange={setContractId}
221
+ />
222
+ <div
223
+ aria-hidden
224
+ style={{
225
+ position: 'fixed',
226
+ left: 0,
227
+ top: 0,
228
+ transform: 'translateX(-200vw)',
229
+ pointerEvents: 'none',
230
+ zIndex: -1,
231
+ }}
232
+ >
233
+ <TimesheetExportCard
234
+ lines={lines}
235
+ periodStart={timesheet.periodStart}
236
+ employeeLabel={labelForEmployee(employees, employeeId)}
237
+ contractLabel={labelForContract(contracts, contractId)}
238
+ clientName={billing.clientName}
239
+ hourlyRate={billing.hourlyRate}
240
+ captureRef={captureRef}
241
+ />
242
+ </div>
243
+ </>
244
+ ) : null}
245
+ </View>
246
+ </View.Item>
247
+ </View>
248
+ )
249
+ }
@@ -0,0 +1,121 @@
1
+ import React, { useMemo } from 'react'
2
+ import { Select, Text, useLocale, View } from '@ossy/design-system'
3
+ import { useSdk } from '@ossy/sdk-react'
4
+ import { SearchResources } from '@ossy/resources'
5
+ import { TimesheetRefs } from './schema-ids.js'
6
+ import { contractLabel, employeeLabel, resourceIdOf } from './timesheet-parties.js'
7
+
8
+ export function useTimesheetParties () {
9
+ const sdk = useSdk()
10
+ const { data: employees = [] } = sdk.read(SearchResources, { type: TimesheetRefs.employee })
11
+ const { data: contracts = [] } = sdk.read(SearchResources, { type: TimesheetRefs.contract })
12
+ return { employees, contracts }
13
+ }
14
+
15
+ export function TimesheetPartyFields ({
16
+ employeeId,
17
+ contractId,
18
+ onEmployeeChange,
19
+ onContractChange,
20
+ disabled,
21
+ layout = 'stack',
22
+ employees: employeesProp,
23
+ contracts: contractsProp,
24
+ children,
25
+ }) {
26
+ const { t } = useLocale()
27
+ const loaded = useTimesheetParties()
28
+ const employees = employeesProp ?? loaded.employees
29
+ const contracts = contractsProp ?? loaded.contracts
30
+ const selectedEmployeeId = resourceIdOf(employeeId) ?? ''
31
+ const selectedContractId = resourceIdOf(contractId) ?? ''
32
+
33
+ const contractOptions = useMemo(() => {
34
+ if (!selectedEmployeeId) return contracts
35
+ return contracts.filter((contract) => {
36
+ const owner = resourceIdOf(contract.content?.employeeId)
37
+ return !owner || owner === selectedEmployeeId || contract.id === selectedContractId
38
+ })
39
+ }, [contracts, selectedEmployeeId, selectedContractId])
40
+
41
+ const setEmployee = (event) => {
42
+ const next = event.target.value
43
+ onEmployeeChange(next ? { resourceId: next } : null)
44
+ if (!next || !selectedContractId) return
45
+ const current = contracts.find((contract) => contract.id === selectedContractId)
46
+ const owner = resourceIdOf(current?.content?.employeeId)
47
+ if (owner && owner !== next) onContractChange(null)
48
+ }
49
+
50
+ const setContract = (event) => {
51
+ const next = event.target.value
52
+ onContractChange(next ? { resourceId: next } : null)
53
+ }
54
+
55
+ const rowSelectStyle = layout === 'rows'
56
+ ? { width: '10rem', maxWidth: '10rem', minWidth: 0, fontWeight: 400 }
57
+ : undefined
58
+
59
+ const employeeSelect = (
60
+ <Select
61
+ value={selectedEmployeeId}
62
+ onChange={setEmployee}
63
+ disabled={disabled}
64
+ aria-label={t('timesheets.employee')}
65
+ style={rowSelectStyle}
66
+ >
67
+ <option value="">{t('timesheets.employeeNone')}</option>
68
+ {employees.map((employee) => (
69
+ <option key={employee.id} value={employee.id}>
70
+ {employeeLabel(employee)}
71
+ </option>
72
+ ))}
73
+ </Select>
74
+ )
75
+
76
+ const contractSelect = (
77
+ <Select
78
+ value={selectedContractId}
79
+ onChange={setContract}
80
+ disabled={disabled}
81
+ aria-label={t('timesheets.contract')}
82
+ style={rowSelectStyle}
83
+ >
84
+ <option value="">{t('timesheets.contractNone')}</option>
85
+ {contractOptions.map((contract) => (
86
+ <option key={contract.id} value={contract.id}>
87
+ {contractLabel(contract)}
88
+ </option>
89
+ ))}
90
+ </Select>
91
+ )
92
+
93
+ if (layout === 'rows') {
94
+ return (
95
+ <>
96
+ <View layout="row" gap="s" style={{ justifyContent: 'space-between', alignItems: 'center', minWidth: 0 }}>
97
+ <Text variant="m" style={{ marginBottom: 0, minWidth: 0, fontWeight: 400, '--font-weight': '400' }} text="timesheets.employee" />
98
+ {employeeSelect}
99
+ </View>
100
+ {children}
101
+ <View layout="row" gap="s" style={{ justifyContent: 'space-between', alignItems: 'center', minWidth: 0 }}>
102
+ <Text variant="m" style={{ marginBottom: 0, minWidth: 0, fontWeight: 400, '--font-weight': '400' }} text="timesheets.contract" />
103
+ {contractSelect}
104
+ </View>
105
+ </>
106
+ )
107
+ }
108
+
109
+ return (
110
+ <>
111
+ <View gap="xs">
112
+ <Text variant="small" weight="medium" text="timesheets.employee" />
113
+ {employeeSelect}
114
+ </View>
115
+ <View gap="xs">
116
+ <Text variant="small" weight="medium" text="timesheets.contract" />
117
+ {contractSelect}
118
+ </View>
119
+ </>
120
+ )
121
+ }