@bit.rhplus/ui.f7.detail-item 0.0.15 → 0.0.17

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.
@@ -1,7 +1,9 @@
1
1
  /* eslint-disable */
2
2
  import React, { useRef, useEffect, useState, useCallback } from 'react';
3
- import { f7, Popup, Page, Navbar, NavLeft, NavTitle, NavTitleLarge, NavRight, Link, Block, Icon, Card } from 'framework7-react';
4
- import { Calendar as CalendarIcon } from 'lucide-react';
3
+ import { f7, Popup, Popover, Page, Navbar, NavLeft, NavTitle, NavTitleLarge, NavRight, Link, Block, Icon, Card, List, ListInput, Picker, Button } from 'framework7-react';
4
+ import { Calendar as CalendarIcon, X } from 'lucide-react';
5
+ import SaveButton from '@bit.rhplus/ui.f7.save-button';
6
+ import SmallButton from '@bit.rhplus/ui.f7.small-button';
5
7
 
6
8
  export const Calendar = ({
7
9
  value,
@@ -16,6 +18,7 @@ export const Calendar = ({
16
18
  icon,
17
19
  iconSize = 16,
18
20
  field = null,
21
+ enableTime = false, // Nový prop pro povolení výběru času
19
22
  calendarParams = {},
20
23
  ...restProps
21
24
  }) => {
@@ -24,7 +27,20 @@ export const Calendar = ({
24
27
  const isInitializingRef = useRef(false);
25
28
  const [displayValue, setDisplayValue] = useState('');
26
29
  const [popupOpened, setPopupOpened] = useState(false);
30
+ const [timePopoverOpened, setTimePopoverOpened] = useState(false);
31
+ const timePickerRef = useRef(null);
27
32
  const [selectedDate, setSelectedDate] = useState(value ? new Date(value) : null);
33
+ const [selectedTime, setSelectedTime] = useState(() => {
34
+ if (value && enableTime) {
35
+ const d = new Date(value);
36
+ return {
37
+ hours: String(d.getHours()).padStart(2, '0'),
38
+ minutes: String(d.getMinutes()).padStart(2, '0'),
39
+ isSet: true
40
+ };
41
+ }
42
+ return { hours: null, minutes: null, isSet: false };
43
+ });
28
44
 
29
45
  // Převod hodnoty na display formát
30
46
  const formatDate = (date) => {
@@ -36,7 +52,16 @@ export const Calendar = ({
36
52
  const month = String(d.getMonth() + 1).padStart(2, '0');
37
53
  const year = d.getFullYear();
38
54
 
39
- return `${day}.${month}.${year}`;
55
+ let dateStr = `${day}.${month}.${year}`;
56
+
57
+ // Pokud je povolený čas a je nastaven, přidej ho k formátu
58
+ if (enableTime && selectedTime.isSet && selectedTime.hours !== null) {
59
+ const hours = String(d.getHours()).padStart(2, '0');
60
+ const minutes = String(d.getMinutes()).padStart(2, '0');
61
+ dateStr += ` ${hours}:${minutes}`;
62
+ }
63
+
64
+ return dateStr;
40
65
  };
41
66
 
42
67
  // Aktualizace display hodnoty při změně value
@@ -54,12 +79,21 @@ export const Calendar = ({
54
79
  // Cleanup kalendáře při zavírání popup
55
80
  useEffect(() => {
56
81
  if (!popupOpened && calendarRef.current) {
57
- calendarRef.current.destroy();
58
- calendarRef.current = null;
59
- // Vyčisti i DOM kontejner
60
- if (calendarContainerRef.current) {
61
- calendarContainerRef.current.innerHTML = '';
62
- }
82
+ // Bezpečný cleanup s timeout pro prevenci konfliktů
83
+ setTimeout(() => {
84
+ if (calendarRef.current) {
85
+ try {
86
+ calendarRef.current.destroy();
87
+ } catch (error) {
88
+ console.warn('Calendar cleanup error:', error);
89
+ }
90
+ calendarRef.current = null;
91
+ }
92
+ // Vyčisti i DOM kontejner
93
+ if (calendarContainerRef.current) {
94
+ calendarContainerRef.current.innerHTML = '';
95
+ }
96
+ }, 100);
63
97
  }
64
98
  }, [popupOpened]);
65
99
 
@@ -106,6 +140,10 @@ export const Calendar = ({
106
140
  dayNames,
107
141
  dayNamesShort: dayNames,
108
142
  firstDay: 1, // Pondělí jako první den
143
+ // Vypne timePicker - použijeme vlastní řešení
144
+ timePicker: false,
145
+ // Vypne automatické zavírání při enableTime
146
+ closeOnSelect: !enableTime,
109
147
  on: {
110
148
  change: (calendar, value) => {
111
149
  // Ignoruj změny během inicializace
@@ -116,14 +154,16 @@ export const Calendar = ({
116
154
  if (value && value.length > 0) {
117
155
  const newSelectedDate = value[0];
118
156
  setSelectedDate(newSelectedDate);
119
- // Automaticky potvrď a zavři popup při výběru data
120
- if (onChange) {
121
- onChange(newSelectedDate);
157
+ // Pokud není povolen čas, automaticky potvrď a zavři popup
158
+ if (!enableTime) {
159
+ if (onChange) {
160
+ onChange(newSelectedDate);
161
+ }
162
+ // Zavři popup s mírným zpožděním pro lepší UX
163
+ setTimeout(() => {
164
+ setPopupOpened(false);
165
+ }, 200);
122
166
  }
123
- // Zavři popup s mírným zpožděním pro lepší UX
124
- setTimeout(() => {
125
- setPopupOpened(false);
126
- }, 200);
127
167
  }
128
168
  }
129
169
  },
@@ -141,25 +181,157 @@ export const Calendar = ({
141
181
  return () => {
142
182
  clearTimeout(timeoutId);
143
183
  if (calendarRef.current) {
144
- calendarRef.current.destroy();
184
+ try {
185
+ calendarRef.current.destroy();
186
+ } catch (error) {
187
+ console.warn('Calendar cleanup error:', error);
188
+ }
145
189
  calendarRef.current = null;
146
190
  }
147
191
  };
148
192
  }, [popupOpened, selectedDate, calendarParams]);
149
193
 
194
+ // Otevření time picker
195
+ const openTimePicker = useCallback(() => {
196
+ if (!enableTime) return;
197
+
198
+ // Bezpečný cleanup existujícího pickeru
199
+ if (timePickerRef.current) {
200
+ try {
201
+ timePickerRef.current.destroy();
202
+ } catch (error) {
203
+ console.warn('Time picker cleanup error:', error);
204
+ }
205
+ timePickerRef.current = null;
206
+ }
207
+
208
+ // Krátký timeout pro zajištění úplného cleanup
209
+ setTimeout(() => {
210
+ // Vytvoř hodnoty pro picker
211
+ const hours = Array.from({ length: 24 }, (_, i) => ({
212
+ value: String(i).padStart(2, '0'),
213
+ displayValue: String(i).padStart(2, '0')
214
+ }));
215
+
216
+ const minutes = Array.from({ length: 60 }, (_, i) => ({
217
+ value: String(i).padStart(2, '0'),
218
+ displayValue: String(i).padStart(2, '0')
219
+ }));
220
+
221
+ timePickerRef.current = f7.picker.create({
222
+ inputEl: '.time-picker-button',
223
+ value: selectedTime.isSet ? [selectedTime.hours, selectedTime.minutes] : ['09', '00'],
224
+ cols: [
225
+ {
226
+ textAlign: 'center',
227
+ values: hours.map(h => h.value),
228
+ displayValues: hours.map(h => h.displayValue),
229
+ },
230
+ {
231
+ textAlign: 'center',
232
+ values: minutes.map(m => m.value),
233
+ displayValues: minutes.map(m => m.displayValue),
234
+ }
235
+ ],
236
+ formatValue: (values) => `${values[0]}:${values[1]}`,
237
+ on: {
238
+ change: (picker, values) => {
239
+ setSelectedTime({
240
+ hours: values[0],
241
+ minutes: values[1],
242
+ isSet: true
243
+ });
244
+ },
245
+ close: () => {
246
+ // Cleanup při zavření
247
+ setTimeout(() => {
248
+ if (timePickerRef.current) {
249
+ try {
250
+ timePickerRef.current.destroy();
251
+ } catch (error) {
252
+ console.warn('Time picker close cleanup error:', error);
253
+ }
254
+ timePickerRef.current = null;
255
+ }
256
+ }, 100);
257
+ }
258
+ }
259
+ });
260
+
261
+ timePickerRef.current.open();
262
+ }, 50);
263
+ }, [enableTime, selectedTime]);
264
+
265
+ // Vynulování času
266
+ const clearTime = useCallback(() => {
267
+ setSelectedTime({ hours: null, minutes: null, isSet: false });
268
+ }, []);
269
+
270
+ // Cleanup time pickeru
271
+ useEffect(() => {
272
+ return () => {
273
+ if (timePickerRef.current) {
274
+ try {
275
+ timePickerRef.current.destroy();
276
+ } catch (error) {
277
+ console.warn('Time picker unmount cleanup error:', error);
278
+ }
279
+ timePickerRef.current = null;
280
+ }
281
+ };
282
+ }, []);
283
+
284
+ // Cleanup při zavírání hlavního popup
285
+ useEffect(() => {
286
+ if (!popupOpened && timePickerRef.current) {
287
+ try {
288
+ timePickerRef.current.destroy();
289
+ } catch (error) {
290
+ console.warn('Time picker popup cleanup error:', error);
291
+ }
292
+ timePickerRef.current = null;
293
+ }
294
+ }, [popupOpened]);
295
+
150
296
  // Potvrzení výběru data
151
297
  const confirmSelection = useCallback(() => {
152
298
  if (selectedDate && onChange) {
153
- onChange(selectedDate);
299
+ let finalDate = new Date(selectedDate);
300
+
301
+ // Pokud je povolen čas a je nastaven, aplikuj vybraný čas
302
+ if (enableTime && selectedTime.isSet && selectedTime.hours !== null) {
303
+ finalDate.setHours(parseInt(selectedTime.hours));
304
+ finalDate.setMinutes(parseInt(selectedTime.minutes));
305
+ } else if (enableTime && !selectedTime.isSet) {
306
+ // Pokud čas není nastaven, použij 00:00
307
+ finalDate.setHours(0);
308
+ finalDate.setMinutes(0);
309
+ }
310
+
311
+ onChange(finalDate);
154
312
  }
155
313
  setPopupOpened(false);
156
- }, [selectedDate, onChange]);
314
+ }, [selectedDate, selectedTime, enableTime, onChange]);
157
315
 
158
316
  // Zrušení výběru
159
317
  const cancelSelection = useCallback(() => {
160
318
  setSelectedDate(value ? new Date(value) : null);
161
- setPopupOpened(false);
162
- }, [value]);
319
+ // Obnovení původního stavu času
320
+ if (value && enableTime) {
321
+ const d = new Date(value);
322
+ setSelectedTime({
323
+ hours: String(d.getHours()).padStart(2, '0'),
324
+ minutes: String(d.getMinutes()).padStart(2, '0'),
325
+ isSet: true
326
+ });
327
+ } else {
328
+ setSelectedTime({ hours: null, minutes: null, isSet: false });
329
+ }
330
+ // Bezpečné zavření s delay
331
+ setTimeout(() => {
332
+ setPopupOpened(false);
333
+ }, 50);
334
+ }, [value, enableTime]);
163
335
 
164
336
  const containerStyle = {
165
337
  display: 'flex',
@@ -250,11 +422,75 @@ export const Calendar = ({
250
422
  />
251
423
  </Block>
252
424
  </Card>
425
+
426
+ {/* Pokud je povolen čas, zobraz tlačítko pro výběr času */}
427
+ {enableTime && (
428
+ <Block
429
+ style={{
430
+ padding: '10px 0',
431
+ }}
432
+ >
433
+ <Card>
434
+ <Block style={{ padding: '20px' }}>
435
+ <div style={{
436
+ display: 'flex',
437
+ justifyContent: 'space-between',
438
+ alignItems: 'center'
439
+ }}>
440
+ <h3 style={{ margin: 0, fontSize: '18px', fontWeight: '600' }}>Čas</h3>
441
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
442
+ <SmallButton
443
+ className="time-picker-button"
444
+ onClick={openTimePicker}
445
+ style={{
446
+ width: 'auto',
447
+ opacity: selectedTime.isSet ? 1 : 0.6,
448
+ padding: '16px 22px',
449
+ }}
450
+ >
451
+ {selectedTime.isSet ? `${selectedTime.hours}:${selectedTime.minutes}` : 'Vyberte čas'}
452
+ </SmallButton>
453
+ {selectedTime.isSet && (
454
+ <button
455
+ onClick={clearTime}
456
+ style={{
457
+ background: 'transparent',
458
+ border: 'none',
459
+ padding: '4px',
460
+ cursor: 'pointer',
461
+ borderRadius: '4px',
462
+ display: 'flex',
463
+ alignItems: 'center',
464
+ justifyContent: 'center',
465
+ opacity: 0.6,
466
+ transition: 'opacity 0.2s'
467
+ }}
468
+ onMouseOver={(e) => e.target.style.opacity = '1'}
469
+ onMouseOut={(e) => e.target.style.opacity = '0.6'}
470
+ >
471
+ <X size={16} color="#666" />
472
+ </button>
473
+ )}
474
+ </div>
475
+ </div>
476
+ </Block>
477
+ </Card>
478
+ </Block>
479
+ )}
480
+
481
+ {/* Tlačítko pro potvrzení */}
482
+ {enableTime && (
483
+ <SaveButton
484
+ onClick={confirmSelection}
485
+ variant="black"
486
+ />
487
+ )}
253
488
  </Block>
254
489
  </div>
255
490
  </div>
256
491
  </div>
257
492
  </Popup>
493
+
258
494
  </>
259
495
  );
260
496
  };
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable */
2
2
  import React from 'react';
3
- import { ListInput } from 'framework7-react';
3
+ import { ListInput, Block, List } from 'framework7-react';
4
4
  import { UserCheck } from 'lucide-react';
5
5
  import { Custom } from './Custom';
6
6
 
@@ -12,29 +12,36 @@ const ContactRenderer = ({ fields, formData, onChange }) => {
12
12
  };
13
13
 
14
14
  return (
15
- <>
16
- {fields.map((field, index) => (
17
- <ListInput
18
- key={field.key || index}
19
- type={field.type || 'text'}
20
- label={field.label}
21
- placeholder={field.placeholder}
22
- value={formData[field.key] || ''}
23
- onInput={(e) => handleInputChange(field.key, e)}
24
- clearButton
25
- style={{ fontSize: '18px' }}
26
- // Speciální styling pro různé typy polí
27
- {...(field.type === 'email' && {
28
- inputMode: 'email',
29
- autoComplete: 'email'
30
- })}
31
- {...(field.type === 'tel' && {
32
- inputMode: 'tel',
33
- autoComplete: 'tel'
34
- })}
35
- />
36
- ))}
37
- </>
15
+ <Block inset>
16
+ <List>
17
+ {fields.map((field, index) => (
18
+ <ListInput
19
+ key={field.key || index}
20
+ type={field.type || 'text'}
21
+ label={field.label}
22
+ placeholder={field.placeholder}
23
+ value={formData[field.key] || ''}
24
+ onInput={(e) => handleInputChange(field.key, e)}
25
+ clearButton
26
+ style={{ fontSize: '18px' }}
27
+ // Speciální styling pro různé typy polí
28
+ {...(field.type === 'email' && {
29
+ inputMode: 'email',
30
+ autoComplete: 'email'
31
+ })}
32
+ {...(field.type === 'tel' && {
33
+ inputMode: 'tel',
34
+ autoComplete: 'tel'
35
+ })}
36
+ {...(field.type === 'textarea' && {
37
+ type: 'textarea',
38
+ resizable: false,
39
+ inputStyle: { minHeight: '60px', resize: 'none' }
40
+ })}
41
+ />
42
+ ))}
43
+ </List>
44
+ </Block>
38
45
  );
39
46
  };
40
47
 
@@ -48,22 +55,17 @@ export const Contact = ({
48
55
  color = '#6887d3',
49
56
  size = 16,
50
57
  field = null, // Pole pro výběr konkrétní vlastnosti objektu
58
+ visibleFields = ['name', 'email', 'phone', 'position'], // Pole k zobrazení v popup
51
59
  ...restProps
52
60
  }) => {
53
61
  // Předdefinovaná pole pro Externí PM
54
- const pmFields = [
62
+ const allFields = [
55
63
  {
56
- key: 'firstName',
64
+ key: 'fullName',
57
65
  label: 'Jméno',
58
66
  type: 'text',
59
67
  placeholder: 'Zadejte jméno'
60
68
  },
61
- {
62
- key: 'lastName',
63
- label: 'Příjmení',
64
- type: 'text',
65
- placeholder: 'Zadejte příjmení'
66
- },
67
69
  {
68
70
  key: 'email',
69
71
  label: 'Email',
@@ -81,11 +83,48 @@ export const Contact = ({
81
83
  label: 'Pozice',
82
84
  type: 'text',
83
85
  placeholder: 'Zadejte pozici'
86
+ },
87
+ {
88
+ key: 'note',
89
+ label: 'Poznámka',
90
+ type: 'textarea',
91
+ placeholder: 'Zadejte poznámku'
84
92
  }
85
93
  ];
86
94
 
87
- // Pole pro zobrazení v linku - jméno, příjmení a pozice
88
- const displayFields = ['firstName', 'lastName', 'position'];
95
+ // Filtrování polí podle visibleFields
96
+ const pmFields = allFields.filter(field => visibleFields.includes(field.key));
97
+
98
+ // Funkce pro generování zobrazovaného textu
99
+ const getContactDisplayText = (contactData) => {
100
+ if (!contactData || typeof contactData !== 'object') {
101
+ return placeholder;
102
+ }
103
+
104
+ // Priorita: fullName > firstName + lastName > email > phone > position
105
+ if (contactData.fullName) {
106
+ return contactData.fullName + (contactData.position ? ` - ${contactData.position}` : '');
107
+ }
108
+
109
+ if (contactData.firstName || contactData.lastName) {
110
+ const name = [contactData.firstName, contactData.lastName].filter(Boolean).join(' ');
111
+ return name + (contactData.position ? ` - ${contactData.position}` : '');
112
+ }
113
+
114
+ if (contactData.email) {
115
+ return contactData.email + (contactData.position ? ` - ${contactData.position}` : '');
116
+ }
117
+
118
+ if (contactData.phone) {
119
+ return contactData.phone + (contactData.position ? ` - ${contactData.position}` : '');
120
+ }
121
+
122
+ if (contactData.position) {
123
+ return contactData.position;
124
+ }
125
+
126
+ return placeholder;
127
+ };
89
128
 
90
129
  return (
91
130
  <Custom
@@ -99,7 +138,7 @@ export const Contact = ({
99
138
  lucideIcon={UserCheck}
100
139
  FormRenderer={ContactRenderer}
101
140
  fields={pmFields}
102
- displayFields={displayFields}
141
+ getDisplayText={getContactDisplayText}
103
142
  field={field}
104
143
  {...restProps}
105
144
  />
@@ -36,6 +36,7 @@ export const Custom = ({
36
36
  { key: 'position', label: 'Pozice', type: 'text', placeholder: 'Zadejte pozici' }
37
37
  ],
38
38
  displayFields = ['firstName', 'lastName', 'position'], // Pole pro zobrazení v linku
39
+ getDisplayText = null, // Custom funkce pro generování zobrazovaného textu
39
40
  field = null, // Pole pro výběr konkrétní vlastnosti objektu
40
41
  ...restProps
41
42
  }) => {
@@ -164,8 +165,15 @@ export const Custom = ({
164
165
  };
165
166
 
166
167
  // Zobrazí text v linku podle vyplněných dat
167
- const getDisplayText = () => {
168
+ const getDisplayTextInternal = () => {
168
169
  const currentObject = findObjectByValue(value);
170
+
171
+ // Pokud je poskytnut custom getDisplayText, použij ho
172
+ if (getDisplayText && typeof getDisplayText === 'function') {
173
+ return getDisplayText(currentObject);
174
+ }
175
+
176
+ // Jinak použij defaultní logiku
169
177
  if (currentObject && typeof currentObject === 'object') {
170
178
  const displayValues = displayFields
171
179
  .map(fieldKey => currentObject[fieldKey])
@@ -182,7 +190,7 @@ export const Custom = ({
182
190
  <div {...restProps}>
183
191
  <Link onClick={handleOpenPopup} className="link" style={linkStyle}>
184
192
  {renderIcon()}
185
- {getDisplayText()}
193
+ {getDisplayTextInternal()}
186
194
  </Link>
187
195
 
188
196
  <Popup
@@ -108,17 +108,18 @@ export const InputText = ({
108
108
  <NavTitleLarge>{title}</NavTitleLarge>
109
109
  </Navbar>
110
110
 
111
- <Block style={{ marginTop: '20px' }}>
111
+ <Block inset>
112
112
  <List>
113
113
  <ListInput
114
- type="text"
115
- label="Typ dokladu"
114
+ type="textarea"
116
115
  placeholder={placeholder}
117
116
  value={inputValue}
118
117
  onInput={(e) => setInputValue(e.target.value)}
119
118
  clearButton
120
119
  autoFocus
120
+ resizable
121
121
  style={{ fontSize: '18px' }}
122
+ inputStyle={{ minHeight: '24px' }}
122
123
  />
123
124
  </List>
124
125
  </Block>
@@ -82,12 +82,15 @@ export const List = ({
82
82
  }
83
83
  };
84
84
 
85
- // Inicializuj selectedItems pouze jednou při mount
86
- const initialSelectedItems = useMemo(() => {
87
- return findItemsByValue(value);
88
- }, []); // Prázdné dependency array - pouze při mount
85
+ // State pro vybrané položky - inicializace s prázdným polem
86
+ const [selectedItems, setSelectedItems] = useState([]);
89
87
 
90
- const [selectedItems, setSelectedItems] = useState(initialSelectedItems);
88
+ // Synchronizace selectedItems s value prop
89
+ useEffect(() => {
90
+ const items = findItemsByValue(value);
91
+ console.log("🚀 ~ List ~ items:", items, value)
92
+ setSelectedItems(items);
93
+ }, [value, options, field]);
91
94
 
92
95
  const linkStyle = {
93
96
  color,
@@ -98,13 +101,16 @@ export const List = ({
98
101
  };
99
102
 
100
103
  const handleCancel = () => {
101
- setSelectedItems(findItemsByValue(value));
104
+ // Reset selected items na aktuální value
105
+ const items = findItemsByValue(value);
106
+ setSelectedItems(items);
102
107
  setPopupOpened(false);
103
108
  };
104
109
 
105
110
  const handleOpenPopup = () => {
106
- // Reset selected items při otevření
107
- setSelectedItems(findItemsByValue(value));
111
+ // Reset selected items při otevření na aktuální value
112
+ const items = findItemsByValue(value);
113
+ setSelectedItems(items);
108
114
  setPopupOpened(true);
109
115
  };
110
116
 
@@ -1,4 +1,4 @@
1
- export function Calendar({ value, title, onChange, placeholder, format, color, style, disabled, lucideIcon, icon, iconSize, field, calendarParams, ...restProps }: {
1
+ export function Calendar({ value, title, onChange, placeholder, format, color, style, disabled, lucideIcon, icon, iconSize, field, enableTime, calendarParams, ...restProps }: {
2
2
  [x: string]: any;
3
3
  value: any;
4
4
  title: any;
@@ -12,6 +12,7 @@ export function Calendar({ value, title, onChange, placeholder, format, color, s
12
12
  icon: any;
13
13
  iconSize?: number | undefined;
14
14
  field?: null | undefined;
15
+ enableTime?: boolean | undefined;
15
16
  calendarParams?: {} | undefined;
16
17
  }): import("react/jsx-runtime").JSX.Element;
17
18
  import React from 'react';