|
|
|
@@ -29,6 +29,7 @@ const emptyForm: ProductCreate = {
|
|
|
|
|
default_store_id: undefined,
|
|
|
|
|
image_path: undefined,
|
|
|
|
|
thumbnail_path: undefined,
|
|
|
|
|
tags: [],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function CatalogueModal({ stores, onClose }: CatalogueModalProps) {
|
|
|
|
@@ -37,6 +38,7 @@ export default function CatalogueModal({ stores, onClose }: CatalogueModalProps)
|
|
|
|
|
const [editing, setEditing] = useState<Product | null>(null)
|
|
|
|
|
const [creating, setCreating] = useState(false)
|
|
|
|
|
const [form, setForm] = useState<ProductCreate>(emptyForm)
|
|
|
|
|
const [tagInput, setTagInput] = useState('')
|
|
|
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
|
const [uploading, setUploading] = useState(false)
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
@@ -54,12 +56,18 @@ export default function CatalogueModal({ stores, onClose }: CatalogueModalProps)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const filteredProducts = products.filter(p =>
|
|
|
|
|
!search.trim() || matchesSearch(p.name, search) || matchesSearch(p.brand ?? '', search) || matchesSearch(p.category ?? '', search)
|
|
|
|
|
)
|
|
|
|
|
const filteredProducts = products.filter(p => {
|
|
|
|
|
if (!search.trim()) return true
|
|
|
|
|
if (matchesSearch(p.name, search)) return true
|
|
|
|
|
if (matchesSearch(p.brand ?? '', search)) return true
|
|
|
|
|
if (matchesSearch(p.category ?? '', search)) return true
|
|
|
|
|
if (p.tags?.some(t => matchesSearch(t, search))) return true
|
|
|
|
|
return false
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function startCreate() {
|
|
|
|
|
setForm(emptyForm)
|
|
|
|
|
setTagInput('')
|
|
|
|
|
setEditing(null)
|
|
|
|
|
setCreating(true)
|
|
|
|
|
setError(null)
|
|
|
|
@@ -78,7 +86,9 @@ export default function CatalogueModal({ stores, onClose }: CatalogueModalProps)
|
|
|
|
|
default_store_id: p.default_store_id ?? undefined,
|
|
|
|
|
image_path: p.image_path ?? undefined,
|
|
|
|
|
thumbnail_path: p.thumbnail_path ?? undefined,
|
|
|
|
|
tags: p.tags ?? [],
|
|
|
|
|
})
|
|
|
|
|
setTagInput('')
|
|
|
|
|
setEditing(p)
|
|
|
|
|
setCreating(false)
|
|
|
|
|
setError(null)
|
|
|
|
@@ -87,9 +97,34 @@ export default function CatalogueModal({ stores, onClose }: CatalogueModalProps)
|
|
|
|
|
function cancelForm() {
|
|
|
|
|
setEditing(null)
|
|
|
|
|
setCreating(false)
|
|
|
|
|
setTagInput('')
|
|
|
|
|
setError(null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addTag() {
|
|
|
|
|
const raw = tagInput.trim().toLowerCase()
|
|
|
|
|
if (!raw) return
|
|
|
|
|
const tags = form.tags ?? []
|
|
|
|
|
if (!tags.includes(raw)) {
|
|
|
|
|
setForm(f => ({ ...f, tags: [...(f.tags ?? []), raw] }))
|
|
|
|
|
}
|
|
|
|
|
setTagInput('')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeTag(tag: string) {
|
|
|
|
|
setForm(f => ({ ...f, tags: (f.tags ?? []).filter(t => t !== tag) }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleTagKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ',') {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
addTag()
|
|
|
|
|
} else if (e.key === 'Backspace' && !tagInput && (form.tags ?? []).length > 0) {
|
|
|
|
|
const tags = form.tags ?? []
|
|
|
|
|
setForm(f => ({ ...f, tags: tags.slice(0, -1) }))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function capitalize(s: string): string {
|
|
|
|
|
const t = s.trim()
|
|
|
|
|
return t.charAt(0).toUpperCase() + t.slice(1)
|
|
|
|
@@ -108,6 +143,7 @@ export default function CatalogueModal({ stores, onClose }: CatalogueModalProps)
|
|
|
|
|
default_store_id: f.default_store_id || undefined,
|
|
|
|
|
image_path: f.image_path || undefined,
|
|
|
|
|
thumbnail_path: f.thumbnail_path || undefined,
|
|
|
|
|
tags: (f.tags ?? []).length > 0 ? f.tags : undefined,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -264,6 +300,41 @@ export default function CatalogueModal({ stores, onClose }: CatalogueModalProps)
|
|
|
|
|
{stores.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Champ tags */}
|
|
|
|
|
<div style={{
|
|
|
|
|
background: 'var(--bg-4)', border: '1px solid var(--bg-5)', borderRadius: 8,
|
|
|
|
|
padding: '6px 10px', display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center',
|
|
|
|
|
minHeight: 40,
|
|
|
|
|
}}>
|
|
|
|
|
{(form.tags ?? []).map(tag => (
|
|
|
|
|
<span key={tag} style={{
|
|
|
|
|
display: 'inline-flex', alignItems: 'center', gap: 4,
|
|
|
|
|
background: 'var(--bg-5)', color: 'var(--ink-2)',
|
|
|
|
|
borderRadius: 999, padding: '2px 8px',
|
|
|
|
|
fontFamily: 'var(--font-ui)', fontSize: 12,
|
|
|
|
|
}}>
|
|
|
|
|
{tag}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => removeTag(tag)}
|
|
|
|
|
style={{ background: 'none', border: 'none', color: 'var(--ink-3)', cursor: 'pointer', padding: 0, fontSize: 11, lineHeight: 1 }}
|
|
|
|
|
>✕</button>
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
<input
|
|
|
|
|
style={{
|
|
|
|
|
flex: 1, minWidth: 80, background: 'none', border: 'none', outline: 'none',
|
|
|
|
|
color: 'var(--ink-1)', fontFamily: 'var(--font-ui)', fontSize: 13,
|
|
|
|
|
}}
|
|
|
|
|
placeholder={(form.tags ?? []).length === 0 ? 'Tags (Entrée pour ajouter)' : ''}
|
|
|
|
|
value={tagInput}
|
|
|
|
|
onChange={e => setTagInput(e.target.value)}
|
|
|
|
|
onKeyDown={handleTagKeyDown}
|
|
|
|
|
onBlur={addTag}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={cancelForm}
|
|
|
|
@@ -282,7 +353,7 @@ export default function CatalogueModal({ stores, onClose }: CatalogueModalProps)
|
|
|
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
|
|
|
<input
|
|
|
|
|
style={{ ...inputStyle, flex: 1 }}
|
|
|
|
|
placeholder="Rechercher un article…"
|
|
|
|
|
placeholder="Rechercher un article ou tag…"
|
|
|
|
|
value={search}
|
|
|
|
|
onChange={e => setSearch(e.target.value)}
|
|
|
|
|
autoFocus
|
|
|
|
@@ -323,7 +394,7 @@ export default function CatalogueModal({ stores, onClose }: CatalogueModalProps)
|
|
|
|
|
{p.name}
|
|
|
|
|
{p.brand && <span style={{ color: 'var(--ink-3)', fontSize: 12, marginLeft: 6 }}>{p.brand}</span>}
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ display: 'flex', gap: 8, marginTop: 2, flexWrap: 'wrap' }}>
|
|
|
|
|
<div style={{ display: 'flex', gap: 8, marginTop: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
|
|
|
{p.category && <span style={{ color: 'var(--info)', fontSize: 11, fontFamily: 'var(--font-ui)' }}>{p.category}</span>}
|
|
|
|
|
{p.default_unit && <span style={{ color: 'var(--ink-3)', fontSize: 11, fontFamily: 'var(--font-mono)' }}>{p.default_unit}</span>}
|
|
|
|
|
{p.price && <span style={{ color: 'var(--ok)', fontSize: 11, fontFamily: 'var(--font-mono)' }}>{p.price} €</span>}
|
|
|
|
@@ -333,6 +404,13 @@ export default function CatalogueModal({ stores, onClose }: CatalogueModalProps)
|
|
|
|
|
{p.avg_interval_days && ` · ~${Math.round(Number(p.avg_interval_days))}j`}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{p.tags && p.tags.length > 0 && p.tags.map(tag => (
|
|
|
|
|
<span key={tag} style={{
|
|
|
|
|
background: 'var(--bg-5)', color: 'var(--ink-3)',
|
|
|
|
|
borderRadius: 999, padding: '1px 6px',
|
|
|
|
|
fontFamily: 'var(--font-ui)', fontSize: 10,
|
|
|
|
|
}}>{tag}</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|