refactor: P3 dekompozice velkych komponent
Dashboard.jsx (1346 -> 378 LOC): - DashKpiCards, DashQuickActions, DashActivityFeed, DashAttendanceToday, DashProfile, DashSessions - dashboardHelpers.js (konstanty + helper funkce) OfferDetail.jsx (1061 -> ~530 LOC): - useOfferForm hook (form state, draft, items/sections, submit) - OfferCustomerPicker (customer search/select dropdown) AttendanceAdmin.jsx (1036 -> ~275 LOC): - useAttendanceAdmin hook (data fetching, filters, CRUD, print) - AttendanceShiftTable (shift records table) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useAlert } from '../context/AlertContext'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||||
@@ -9,28 +9,12 @@ import Forbidden from '../components/Forbidden'
|
||||
import AdminDatePicker from '../components/AdminDatePicker'
|
||||
import OfferItemsSection from '../components/OfferItemsSection'
|
||||
import OfferScopeSection from '../components/OfferScopeSection'
|
||||
import OfferCustomerPicker from '../components/OfferCustomerPicker'
|
||||
import useModalLock from '../hooks/useModalLock'
|
||||
import useOfferForm from '../hooks/useOfferForm'
|
||||
import apiFetch from '../utils/api'
|
||||
const API_BASE = '/api/admin'
|
||||
|
||||
let _keyCounter = 0
|
||||
const emptyItem = () => ({
|
||||
_key: `item-${++_keyCounter}`,
|
||||
description: '',
|
||||
item_description: '',
|
||||
quantity: 1,
|
||||
unit: '',
|
||||
unit_price: 0,
|
||||
is_included_in_total: true
|
||||
})
|
||||
|
||||
const emptySection = () => ({
|
||||
_key: `sec-${++_keyCounter}`,
|
||||
title: '',
|
||||
title_cz: '',
|
||||
content: ''
|
||||
})
|
||||
|
||||
export default function OfferDetail() {
|
||||
const { id } = useParams()
|
||||
const isEdit = Boolean(id)
|
||||
@@ -38,361 +22,35 @@ export default function OfferDetail() {
|
||||
const { hasPermission } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [loading, setLoading] = useState(isEdit)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [errors, setErrors] = useState({})
|
||||
const [customers, setCustomers] = useState([])
|
||||
const [customerSearch, setCustomerSearch] = useState('')
|
||||
const [showCustomerDropdown, setShowCustomerDropdown] = useState(false)
|
||||
const [itemTemplates, setItemTemplates] = useState([])
|
||||
const [scopeTemplates, setScopeTemplates] = useState([])
|
||||
const {
|
||||
loading, saving, errors, setErrors,
|
||||
form, updateForm, items, setItems, sections,
|
||||
customers, itemTemplates, scopeTemplates,
|
||||
orderInfo, offerStatus, setOfferStatus,
|
||||
totals, draftSavedAtLabel,
|
||||
selectCustomer, clearCustomer,
|
||||
updateItem, addItem, removeItem, addItemFromTemplate,
|
||||
addSection, removeSection, updateSection, moveSection,
|
||||
loadScopeTemplate, handleSave
|
||||
} = useOfferForm({ id, isEdit, alert, navigate })
|
||||
|
||||
const [showItemTemplateMenu, setShowItemTemplateMenu] = useState(false)
|
||||
const [showScopeTemplateMenu, setShowScopeTemplateMenu] = useState(false)
|
||||
|
||||
const [form, setForm] = useState({
|
||||
quotation_number: '',
|
||||
project_code: '',
|
||||
customer_id: null,
|
||||
customer_name: '',
|
||||
created_at: new Date().toISOString().split('T')[0],
|
||||
valid_until: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||
currency: 'EUR',
|
||||
language: 'EN',
|
||||
vat_rate: 21,
|
||||
apply_vat: false,
|
||||
exchange_rate: '',
|
||||
exchange_rate_date: '',
|
||||
scope_title: '',
|
||||
scope_description: ''
|
||||
})
|
||||
|
||||
const [items, setItems] = useState([emptyItem()])
|
||||
const [sections, setSections] = useState([])
|
||||
|
||||
const [deleteConfirm, setDeleteConfirm] = useState(false)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const [orderInfo, setOrderInfo] = useState(null)
|
||||
const [creatingOrder, setCreatingOrder] = useState(false)
|
||||
const [showOrderModal, setShowOrderModal] = useState(false)
|
||||
const [offerStatus, setOfferStatus] = useState('active')
|
||||
const [invalidateConfirm, setInvalidateConfirm] = useState(false)
|
||||
const [invalidatingOffer, setInvalidatingOffer] = useState(false)
|
||||
useModalLock(showOrderModal)
|
||||
const [customerOrderNumber, setCustomerOrderNumber] = useState('')
|
||||
const [orderAttachment, setOrderAttachment] = useState(null)
|
||||
const [pdfLoading, setPdfLoading] = useState(false)
|
||||
|
||||
const DRAFT_KEY = 'boha_offer_draft'
|
||||
const [draftSavedAt, setDraftSavedAt] = useState(null)
|
||||
const draftDataRef = useRef({ form, items, sections })
|
||||
const draftRestoredRef = useRef(false)
|
||||
useModalLock(showOrderModal)
|
||||
|
||||
// Fetch customers + templates on mount
|
||||
useEffect(() => {
|
||||
const fetchMeta = async () => {
|
||||
try {
|
||||
const [custRes, itemTplRes, scopeTplRes] = await Promise.all([
|
||||
apiFetch(`${API_BASE}/customers.php`),
|
||||
apiFetch(`${API_BASE}/offers-templates.php?action=items`),
|
||||
apiFetch(`${API_BASE}/offers-templates.php?action=scopes`)
|
||||
])
|
||||
const custData = await custRes.json()
|
||||
const itemTplData = await itemTplRes.json()
|
||||
const scopeTplData = await scopeTplRes.json()
|
||||
|
||||
if (custData.success) setCustomers(custData.data.customers)
|
||||
if (itemTplData.success) setItemTemplates(itemTplData.data.templates)
|
||||
if (scopeTplData.success) setScopeTemplates(scopeTplData.data.templates)
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
fetchMeta()
|
||||
}, [])
|
||||
|
||||
// Restore draft on mount (new offers only)
|
||||
useEffect(() => {
|
||||
if (isEdit) return
|
||||
try {
|
||||
const raw = localStorage.getItem(DRAFT_KEY)
|
||||
if (!raw) return
|
||||
const draft = JSON.parse(raw)
|
||||
if (!draft || typeof draft !== 'object' || !draft.form || !Array.isArray(draft.items)) {
|
||||
localStorage.removeItem(DRAFT_KEY)
|
||||
return
|
||||
}
|
||||
const { form: dForm, items: dItems, sections: dSections, savedAt } = draft
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
project_code: dForm.project_code ?? prev.project_code,
|
||||
customer_id: dForm.customer_id ?? prev.customer_id,
|
||||
customer_name: dForm.customer_name ?? prev.customer_name,
|
||||
created_at: dForm.created_at ?? prev.created_at,
|
||||
valid_until: dForm.valid_until ?? prev.valid_until,
|
||||
currency: dForm.currency ?? prev.currency,
|
||||
language: dForm.language ?? prev.language,
|
||||
vat_rate: dForm.vat_rate ?? prev.vat_rate,
|
||||
apply_vat: dForm.apply_vat ?? prev.apply_vat,
|
||||
exchange_rate: dForm.exchange_rate ?? prev.exchange_rate,
|
||||
exchange_rate_date: dForm.exchange_rate_date ?? prev.exchange_rate_date,
|
||||
scope_title: dForm.scope_title ?? prev.scope_title,
|
||||
scope_description: dForm.scope_description ?? prev.scope_description,
|
||||
}))
|
||||
if (dItems.length) setItems(dItems.map(i => ({ ...i, _key: i._key || `item-${++_keyCounter}` })))
|
||||
if (Array.isArray(dSections) && dSections.length) setSections(dSections.map(s => ({ ...s, _key: s._key || `sec-${++_keyCounter}` })))
|
||||
draftRestoredRef.current = true
|
||||
if (savedAt) setDraftSavedAt(new Date(savedAt))
|
||||
} catch {
|
||||
try { localStorage.removeItem(DRAFT_KEY) } catch { /* ignore */ }
|
||||
}
|
||||
}, [isEdit])
|
||||
|
||||
useEffect(() => {
|
||||
draftDataRef.current = { form, items, sections }
|
||||
}, [form, items, sections])
|
||||
|
||||
// Auto-save draft (jen nove nabidky)
|
||||
useEffect(() => {
|
||||
if (isEdit) return
|
||||
const timer = setTimeout(() => {
|
||||
try {
|
||||
const { form: f, items: it, sections: sc } = draftDataRef.current
|
||||
const { quotation_number: _qn, ...formWithoutNumber } = f
|
||||
const savedAt = new Date().toISOString()
|
||||
localStorage.setItem(DRAFT_KEY, JSON.stringify({ form: formWithoutNumber, items: it, sections: sc, savedAt }))
|
||||
setDraftSavedAt(new Date(savedAt))
|
||||
} catch { /* ignore */ }
|
||||
}, 500)
|
||||
return () => clearTimeout(timer)
|
||||
}, [form, items, sections, isEdit])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEdit) {
|
||||
const fetchNextNumber = async () => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/offers.php?action=next_number`)
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
setForm(prev => ({ ...prev, quotation_number: result.data.number }))
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// Fetch default settings
|
||||
const fetchDefaults = async () => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/company-settings.php`)
|
||||
const result = await response.json()
|
||||
if (result.success && !draftRestoredRef.current) {
|
||||
const s = result.data
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
currency: s.default_currency || prev.currency,
|
||||
vat_rate: s.default_vat_rate || prev.vat_rate
|
||||
}))
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
fetchNextNumber()
|
||||
fetchDefaults()
|
||||
return
|
||||
}
|
||||
|
||||
const fetchDetail = async () => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/offers.php?action=detail&id=${id}`)
|
||||
if (response.status === 401) return
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
const q = result.data
|
||||
setForm({
|
||||
quotation_number: q.quotation_number || '',
|
||||
project_code: q.project_code || '',
|
||||
customer_id: q.customer_id || null,
|
||||
customer_name: q.customer_name || '',
|
||||
created_at: (q.created_at || '').substring(0, 10),
|
||||
valid_until: (q.valid_until || '').substring(0, 10),
|
||||
currency: q.currency || 'EUR',
|
||||
language: q.language || 'EN',
|
||||
vat_rate: q.vat_rate || 21,
|
||||
apply_vat: Boolean(q.apply_vat),
|
||||
exchange_rate: q.exchange_rate || '',
|
||||
exchange_rate_date: q.exchange_rate_date || '',
|
||||
scope_title: q.scope_title || '',
|
||||
scope_description: q.scope_description || ''
|
||||
})
|
||||
|
||||
if (q.items?.length) {
|
||||
setItems(q.items.map(item => ({
|
||||
_key: `item-${++_keyCounter}`,
|
||||
description: item.description || '',
|
||||
item_description: item.item_description || '',
|
||||
quantity: Number(item.quantity) || 1,
|
||||
unit: item.unit || '',
|
||||
unit_price: Number(item.unit_price) || 0,
|
||||
is_included_in_total: Boolean(item.is_included_in_total)
|
||||
})))
|
||||
}
|
||||
|
||||
if (q.sections?.length) {
|
||||
setSections(q.sections.map(s => ({
|
||||
_key: `sec-${++_keyCounter}`,
|
||||
title: s.title || '',
|
||||
title_cz: s.title_cz || '',
|
||||
content: s.content || ''
|
||||
})))
|
||||
}
|
||||
|
||||
setOrderInfo(q.order || null)
|
||||
setOfferStatus(q.status || 'active')
|
||||
} else {
|
||||
alert.error(result.error || 'Nepodařilo se načíst nabídku')
|
||||
navigate('/offers')
|
||||
}
|
||||
} catch {
|
||||
alert.error('Chyba připojení')
|
||||
navigate('/offers')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchDetail()
|
||||
}, [isEdit, id, alert, navigate])
|
||||
|
||||
// Close customer dropdown on outside click
|
||||
useEffect(() => {
|
||||
const handleClickOutside = () => setShowCustomerDropdown(false)
|
||||
if (showCustomerDropdown) {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
return () => document.removeEventListener('click', handleClickOutside)
|
||||
}
|
||||
}, [showCustomerDropdown])
|
||||
|
||||
// Calculated totals
|
||||
const totals = useMemo(() => {
|
||||
const subtotal = items.reduce((sum, item) => {
|
||||
if (item.is_included_in_total) {
|
||||
return sum + (Number(item.quantity) || 0) * (Number(item.unit_price) || 0)
|
||||
}
|
||||
return sum
|
||||
}, 0)
|
||||
const vatAmount = form.apply_vat ? subtotal * ((Number(form.vat_rate) || 0) / 100) : 0
|
||||
return { subtotal, vatAmount, total: subtotal + vatAmount }
|
||||
}, [items, form.apply_vat, form.vat_rate])
|
||||
|
||||
// Customer filtering
|
||||
const filteredCustomers = useMemo(() => {
|
||||
if (!customerSearch) return customers
|
||||
const q = customerSearch.toLowerCase()
|
||||
return customers.filter(c =>
|
||||
(c.name || '').toLowerCase().includes(q) ||
|
||||
(c.company_id || '').includes(customerSearch) ||
|
||||
(c.city || '').toLowerCase().includes(q)
|
||||
)
|
||||
}, [customers, customerSearch])
|
||||
|
||||
// Draft helpers
|
||||
const clearDraft = useCallback(() => {
|
||||
try { localStorage.removeItem(DRAFT_KEY) } catch { /* ignore */ }
|
||||
setDraftSavedAt(null)
|
||||
}, [])
|
||||
|
||||
const draftSavedAtLabel = useMemo(() => {
|
||||
if (!draftSavedAt) return null
|
||||
return draftSavedAt.toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' })
|
||||
}, [draftSavedAt])
|
||||
|
||||
// Form handlers
|
||||
const updateForm = (field, value) => setForm(prev => ({ ...prev, [field]: value }))
|
||||
|
||||
const selectCustomer = (customer) => {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
customer_id: customer.id,
|
||||
customer_name: customer.name
|
||||
}))
|
||||
setErrors(prev => ({ ...prev, customer_id: undefined }))
|
||||
setCustomerSearch('')
|
||||
setShowCustomerDropdown(false)
|
||||
}
|
||||
|
||||
const clearCustomer = () => {
|
||||
setForm(prev => ({ ...prev, customer_id: null, customer_name: '' }))
|
||||
}
|
||||
|
||||
// Items handlers
|
||||
const updateItem = (index, field, value) => {
|
||||
setItems(prev => prev.map((item, i) => i === index ? { ...item, [field]: value } : item))
|
||||
}
|
||||
|
||||
const addItem = () => setItems(prev => [...prev, emptyItem()])
|
||||
|
||||
const removeItem = (index) => {
|
||||
setItems(prev => prev.length > 1 ? prev.filter((_, i) => i !== index) : prev)
|
||||
}
|
||||
|
||||
const addItemFromTemplate = (template) => {
|
||||
setItems(prev => [...prev, {
|
||||
_key: `item-${++_keyCounter}`,
|
||||
description: template.name || '',
|
||||
item_description: template.description || '',
|
||||
quantity: 1,
|
||||
unit: '',
|
||||
unit_price: Number(template.default_price) || 0,
|
||||
is_included_in_total: true
|
||||
}])
|
||||
setShowItemTemplateMenu(false)
|
||||
}
|
||||
|
||||
// Sections handlers
|
||||
const addSection = () => setSections(prev => [...prev, emptySection()])
|
||||
|
||||
const removeSection = (index) => {
|
||||
setSections(prev => prev.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const updateSection = (index, field, value) => {
|
||||
setSections(prev => prev.map((s, i) => i === index ? { ...s, [field]: value } : s))
|
||||
}
|
||||
|
||||
const moveSection = (index, direction) => {
|
||||
setSections(prev => {
|
||||
const newSections = [...prev]
|
||||
const target = index + direction
|
||||
if (target < 0 || target >= newSections.length) return prev
|
||||
;[newSections[index], newSections[target]] = [newSections[target], newSections[index]]
|
||||
return newSections
|
||||
})
|
||||
}
|
||||
|
||||
const loadScopeTemplate = async (template) => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/offers-templates.php?action=scope_detail&id=${template.id}`)
|
||||
const result = await response.json()
|
||||
if (result.success) {
|
||||
// Load template-level fields into the quotation form
|
||||
const tpl = result.data
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
scope_description: tpl.description || prev.scope_description
|
||||
}))
|
||||
|
||||
// Load sections
|
||||
if (tpl.sections) {
|
||||
const newSections = tpl.sections.map(s => ({
|
||||
_key: `sec-${++_keyCounter}`,
|
||||
title: s.title || '',
|
||||
title_cz: s.title_cz || '',
|
||||
content: s.content || ''
|
||||
}))
|
||||
setSections(prev => [...prev, ...newSections])
|
||||
}
|
||||
alert.success(`Načtena šablona "${template.name}"`)
|
||||
}
|
||||
} catch {
|
||||
alert.error('Nepodařilo se načíst šablonu')
|
||||
}
|
||||
setShowScopeTemplateMenu(false)
|
||||
}
|
||||
const isInvalidated = offerStatus === 'invalidated'
|
||||
const isExpiredNotInvalidated = isEdit && !isInvalidated && !orderInfo && form.valid_until && new Date(form.valid_until) < new Date(new Date().toDateString())
|
||||
|
||||
const handleCreateOrder = async () => {
|
||||
if (!customerOrderNumber.trim()) {
|
||||
@@ -426,79 +84,6 @@ export default function OfferDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
// Save
|
||||
const handleSave = async () => {
|
||||
const newErrors = {}
|
||||
if (!form.customer_id) newErrors.customer_id = 'Vyberte zákazníka'
|
||||
if (!form.created_at) newErrors.created_at = 'Zadejte datum'
|
||||
if (!form.valid_until) newErrors.valid_until = 'Zadejte datum'
|
||||
if (items.length === 0 || items.every(i => !i.description.trim())) {
|
||||
newErrors.items = 'Přidejte alespoň jednu položku'
|
||||
}
|
||||
setErrors(newErrors)
|
||||
if (Object.keys(newErrors).length > 0) return
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
const payload = {
|
||||
quotation: {
|
||||
project_code: form.project_code,
|
||||
customer_id: form.customer_id,
|
||||
created_at: form.created_at,
|
||||
valid_until: form.valid_until,
|
||||
currency: form.currency,
|
||||
language: form.language,
|
||||
vat_rate: form.vat_rate,
|
||||
apply_vat: form.apply_vat,
|
||||
exchange_rate: form.exchange_rate || null,
|
||||
exchange_rate_date: form.exchange_rate_date || null,
|
||||
scope_title: form.scope_title,
|
||||
scope_description: form.scope_description
|
||||
},
|
||||
items: items.map((item, i) => ({
|
||||
...item,
|
||||
position: i + 1
|
||||
})),
|
||||
sections: sections.map((s, i) => ({
|
||||
...s,
|
||||
position: i + 1
|
||||
}))
|
||||
}
|
||||
|
||||
const url = isEdit
|
||||
? `${API_BASE}/offers.php?id=${id}`
|
||||
: `${API_BASE}/offers.php`
|
||||
|
||||
const response = await apiFetch(url, {
|
||||
method: isEdit ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
alert.success(result.message || (isEdit ? 'Nabídka byla uložena' : 'Nabídka byla vytvořena'))
|
||||
if (!isEdit && result.data?.id) {
|
||||
clearDraft()
|
||||
const newId = result.data.id
|
||||
setTimeout(() => navigate(`/offers/${newId}`, { replace: true }), 300)
|
||||
}
|
||||
} else {
|
||||
alert.error(result.error || 'Nepodařilo se uložit nabídku')
|
||||
}
|
||||
} catch {
|
||||
alert.error('Chyba připojení')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const [pdfLoading, setPdfLoading] = useState(false)
|
||||
|
||||
const isInvalidated = offerStatus === 'invalidated'
|
||||
const isExpiredNotInvalidated = isEdit && !isInvalidated && !orderInfo && form.valid_until && new Date(form.valid_until) < new Date(new Date().toDateString())
|
||||
|
||||
const handleInvalidateOffer = async () => {
|
||||
setInvalidatingOffer(true)
|
||||
try {
|
||||
@@ -520,13 +105,6 @@ export default function OfferDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
const getRequiredPerm = () => {
|
||||
if (!isEdit) return 'offers.create'
|
||||
return isInvalidated ? 'offers.view' : 'offers.edit'
|
||||
}
|
||||
const requiredPerm = getRequiredPerm()
|
||||
if (!hasPermission(requiredPerm)) return <Forbidden />
|
||||
|
||||
const handleDelete = async () => {
|
||||
setDeleting(true)
|
||||
try {
|
||||
@@ -573,6 +151,13 @@ export default function OfferDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
const getRequiredPerm = () => {
|
||||
if (!isEdit) return 'offers.create'
|
||||
return isInvalidated ? 'offers.view' : 'offers.edit'
|
||||
}
|
||||
const requiredPerm = getRequiredPerm()
|
||||
if (!hasPermission(requiredPerm)) return <Forbidden />
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="admin-skeleton" style={{ padding: 0, gap: '1.5rem' }}>
|
||||
@@ -755,54 +340,15 @@ export default function OfferDetail() {
|
||||
readOnly={isInvalidated}
|
||||
/>
|
||||
</div>
|
||||
<div className={`admin-form-group${errors.customer_id ? ' has-error' : ''}`}>
|
||||
<label className="admin-form-label required">Zákazník</label>
|
||||
{form.customer_id && (
|
||||
<div className="offers-customer-selected">
|
||||
<span>{form.customer_name}</span>
|
||||
{!isInvalidated && (
|
||||
<button type="button" onClick={clearCustomer} className="admin-btn-icon" title="Odebrat zákazníka" aria-label="Odebrat zákazníka">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!form.customer_id && !isInvalidated && (
|
||||
<div className="offers-customer-select" onClick={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
type="text"
|
||||
value={customerSearch}
|
||||
onChange={(e) => { setCustomerSearch(e.target.value); setShowCustomerDropdown(true) }}
|
||||
onFocus={() => setShowCustomerDropdown(true)}
|
||||
className="admin-form-input"
|
||||
placeholder="Hledat zákazníka..."
|
||||
/>
|
||||
{showCustomerDropdown && (
|
||||
<div className="offers-customer-dropdown">
|
||||
{filteredCustomers.length === 0 ? (
|
||||
<div className="offers-customer-dropdown-empty">
|
||||
Žádní zákazníci
|
||||
</div>
|
||||
) : (
|
||||
filteredCustomers.slice(0, 10).map(c => (
|
||||
<div
|
||||
key={c.id}
|
||||
className="offers-customer-dropdown-item"
|
||||
onMouseDown={() => selectCustomer(c)}
|
||||
>
|
||||
<div>{c.name}</div>
|
||||
{c.city && <div>{c.city}</div>}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{errors.customer_id && <span className="admin-form-error">{errors.customer_id}</span>}
|
||||
</div>
|
||||
<OfferCustomerPicker
|
||||
customers={customers}
|
||||
customerId={form.customer_id}
|
||||
customerName={form.customer_name}
|
||||
onSelect={selectCustomer}
|
||||
onClear={clearCustomer}
|
||||
error={errors.customer_id}
|
||||
readOnly={isInvalidated}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
@@ -849,10 +395,10 @@ export default function OfferDetail() {
|
||||
className="admin-form-select"
|
||||
disabled={isInvalidated}
|
||||
>
|
||||
<option value="EUR">EUR (€)</option>
|
||||
<option value="EUR">EUR (€)</option>
|
||||
<option value="USD">USD ($)</option>
|
||||
<option value="CZK">CZK (Kč)</option>
|
||||
<option value="GBP">GBP (£)</option>
|
||||
<option value="GBP">GBP (£)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="admin-form-group">
|
||||
|
||||
Reference in New Issue
Block a user