feat: calculs tableaux Structured Views + Link Preview + fixes
- Calculs en pied de tableau : Somme/Moyenne/Min/Max/Compte, cliquable pour changer - Link Preview : métadonnées persistées + texte indexable pour recherche/embeddings - Fix: bg-memento-paper → bg-card (dark mode) sur dialogs Structured Views - Fix: bouton Ajouter un champ → brand-accent au lieu de primary - Calendar view retiré du sélecteur (non pertinent)
This commit is contained in:
@@ -50,7 +50,7 @@ export function AddPropertyDialog({ open, onClose, onSubmit }: AddPropertyDialog
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal
|
||||
className="w-full max-w-md rounded-2xl border border-border bg-memento-paper shadow-xl p-6 space-y-5"
|
||||
className="w-full max-w-md rounded-2xl border border-border bg-card shadow-xl p-6 space-y-5"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="font-memento-serif text-lg">{t('structuredViews.addPropertyTitle')}</h2>
|
||||
@@ -117,7 +117,7 @@ export function AddPropertyDialog({ open, onClose, onSubmit }: AddPropertyDialog
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
{t('general.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={saving || !name.trim()}>
|
||||
<Button type="submit" disabled={saving || !name.trim()} className="bg-brand-accent text-white hover:bg-brand-accent/90">
|
||||
{t('structuredViews.addProperty')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ import { getNoteDisplayTitle } from '@/lib/note-preview'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
|
||||
import { enUS, fr, faIR } from 'date-fns/locale'
|
||||
import { ChevronDown, ChevronUp, Filter, Trash2, Sparkles, Brain, Loader2, ArrowUpRight, Link2 } from 'lucide-react'
|
||||
import { ChevronDown, ChevronUp, Filter, Trash2, Sparkles, Brain, Loader2, ArrowUpRight, Link2, Sigma } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { openNotePeek } from '@/lib/note-peek-sync'
|
||||
|
||||
@@ -88,6 +88,7 @@ export function NotesStructuredTable({
|
||||
const [filterValue, setFilterValue] = useState('')
|
||||
const [propertyToDelete, setPropertyToDelete] = useState<{ id: string; name: string } | null>(null)
|
||||
const [deletingProperty, setDeletingProperty] = useState(false)
|
||||
const [calcTypes, setCalcTypes] = useState<Record<string, 'none' | 'sum' | 'avg' | 'min' | 'max' | 'count'>>({})
|
||||
|
||||
// Memory Echo states
|
||||
const { requestAiConsent } = useAiConsent()
|
||||
@@ -189,6 +190,53 @@ export function NotesStructuredTable({
|
||||
return sortNotesWithProperties(filtered, noteValues, sort, schema.properties)
|
||||
}, [notes, noteValues, filters, sort, schema.properties])
|
||||
|
||||
const numberProperties = schema.properties.filter(p => p.type === 'number')
|
||||
|
||||
const calculations = useMemo(() => {
|
||||
const result: Record<string, { value: string | null; type: string }> = {}
|
||||
for (const prop of schema.properties) {
|
||||
const calcType = calcTypes[prop.id] || (prop.type === 'number' ? 'sum' : 'count')
|
||||
if (calcType === 'none') { result[prop.id] = { value: null, type: 'none' }; continue }
|
||||
|
||||
const values = displayed
|
||||
.map(n => noteValues[n.id]?.[prop.id])
|
||||
.filter(v => v !== null && v !== undefined && v !== '')
|
||||
|
||||
if (prop.type === 'number') {
|
||||
const nums = values.map(v => parseFloat(String(v))).filter(n => !isNaN(n))
|
||||
if (nums.length === 0) { result[prop.id] = { value: null, type: calcType }; continue }
|
||||
if (calcType === 'sum') result[prop.id] = { value: formatNum(nums.reduce((a, b) => a + b, 0)), type: 'sum' }
|
||||
else if (calcType === 'avg') result[prop.id] = { value: formatNum(nums.reduce((a, b) => a + b, 0) / nums.length), type: 'avg' }
|
||||
else if (calcType === 'min') result[prop.id] = { value: formatNum(Math.min(...nums)), type: 'min' }
|
||||
else if (calcType === 'max') result[prop.id] = { value: formatNum(Math.max(...nums)), type: 'max' }
|
||||
else if (calcType === 'count') result[prop.id] = { value: String(nums.length), type: 'count' }
|
||||
} else {
|
||||
result[prop.id] = { value: calcType === 'count' ? String(values.length) : null, type: calcType }
|
||||
}
|
||||
}
|
||||
return result
|
||||
}, [displayed, noteValues, schema.properties, calcTypes])
|
||||
|
||||
function formatNum(n: number): string {
|
||||
return Number.isInteger(n) ? String(n) : n.toFixed(2)
|
||||
}
|
||||
|
||||
function cycleCalcType(propId: string, propType: string) {
|
||||
const current = calcTypes[propId] || (propType === 'number' ? 'sum' : 'count')
|
||||
const numberCycle: Array<typeof current> = ['sum', 'avg', 'min', 'max', 'count', 'none']
|
||||
const otherCycle: Array<typeof current> = ['count', 'none']
|
||||
const cycle = propType === 'number' ? numberCycle : otherCycle
|
||||
const nextIdx = (cycle.indexOf(current) + 1) % cycle.length
|
||||
setCalcTypes(prev => ({ ...prev, [propId]: cycle[nextIdx] }))
|
||||
}
|
||||
|
||||
const calcLabel = (type: string): string => {
|
||||
const labels: Record<string, string> = {
|
||||
sum: 'Σ', avg: 'x̄', min: 'min', max: 'max', count: '#', none: ''
|
||||
}
|
||||
return labels[type] || ''
|
||||
}
|
||||
|
||||
const toggleSort = (propertyId: ColumnSort['propertyId']) => {
|
||||
setSort((prev) =>
|
||||
prev.propertyId === propertyId
|
||||
@@ -429,6 +477,38 @@ export function NotesStructuredTable({
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
{numberProperties.length > 0 && (
|
||||
<tfoot>
|
||||
<tr className="border-t-2 border-border/40 bg-muted/20">
|
||||
<td className="px-4 py-2 text-[10px] uppercase tracking-widest font-bold text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Sigma size={12} />
|
||||
{t('structuredViews.calculations')}
|
||||
</span>
|
||||
</td>
|
||||
{schema.properties.map((p) => {
|
||||
const calc = calculations[p.id]
|
||||
if (!calc || calc.value === null) {
|
||||
return <td key={p.id} className="px-4 py-2 text-[11px] text-muted-foreground/40">—</td>
|
||||
}
|
||||
return (
|
||||
<td key={p.id} className="px-4 py-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => cycleCalcType(p.id, p.type)}
|
||||
className="text-[11px] font-semibold tabular-nums hover:text-primary transition-colors"
|
||||
title={t('structuredViews.calcClickToChange')}
|
||||
>
|
||||
<span className="text-muted-foreground mr-1">{calcLabel(calc.type)}</span>
|
||||
{calc.value}
|
||||
</button>
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
<td />
|
||||
</tr>
|
||||
</tfoot>
|
||||
)}
|
||||
</table>
|
||||
{displayed.length === 0 && (
|
||||
<p className="text-center py-8 text-muted-foreground text-sm">{t('structuredViews.noMatchingNotes')}</p>
|
||||
|
||||
@@ -140,7 +140,7 @@ export function StructuredViewsWizard({
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal
|
||||
className="w-full max-w-lg rounded-2xl border border-border bg-memento-paper shadow-xl p-6 space-y-5"
|
||||
className="w-full max-w-lg rounded-2xl border border-border bg-card shadow-xl p-6 space-y-5"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
|
||||
@@ -2647,6 +2647,8 @@
|
||||
"deleteProperty": "Delete field",
|
||||
"deletePropertyTitle": "Delete this field?",
|
||||
"deletePropertyConfirm": "The field \"{name}\" and all its values on notes in this notebook will be removed. This cannot be undone.",
|
||||
"calculations": "Calculations",
|
||||
"calcClickToChange": "Click to change calculation type (Sum, Average, Min, Max, Count, None)",
|
||||
"deletePropertySuccess": "Field deleted",
|
||||
"cellEmpty": "—",
|
||||
"multiselectPick": "Choose…",
|
||||
|
||||
@@ -2651,6 +2651,8 @@
|
||||
"deleteProperty": "Supprimer le champ",
|
||||
"deletePropertyTitle": "Supprimer ce champ ?",
|
||||
"deletePropertyConfirm": "Le champ « {name} » et toutes ses valeurs sur les notes de ce carnet seront supprimés. Cette action est irréversible.",
|
||||
"calculations": "Calculs",
|
||||
"calcClickToChange": "Cliquer pour changer le type de calcul (Somme, Moyenne, Min, Max, Compte, Aucun)",
|
||||
"deletePropertySuccess": "Champ supprimé",
|
||||
"cellEmpty": "—",
|
||||
"multiselectPick": "Choisir…",
|
||||
|
||||
Reference in New Issue
Block a user