| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- import { useState, useMemo } from 'react';
- import { useQuery } from '@tanstack/react-query';
- import { useTranslation } from 'react-i18next';
- import { api, type InventorySpool } from '../../api/client';
- function formatDate(dateStr: string | null): string {
- if (!dateStr) return '-';
- try {
- const d = new Date(dateStr);
- return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
- } catch {
- return '-';
- }
- }
- function SpoolCard({ spool }: { spool: InventorySpool }) {
- const { t } = useTranslation();
- const [expanded, setExpanded] = useState(false);
- const remaining = Math.max(0, spool.label_weight - spool.weight_used);
- const remainingPct = spool.label_weight > 0 ? (remaining / spool.label_weight) * 100 : 0;
- const colorHex = spool.rgba ? `#${spool.rgba.slice(0, 6)}` : '#808080';
- return (
- <button
- className="w-full bg-zinc-800 rounded-lg p-3 text-left transition-colors hover:bg-zinc-750 active:bg-zinc-700"
- onClick={() => setExpanded(!expanded)}
- >
- <div className="flex items-center gap-3">
- {/* Color swatch */}
- <div
- className="w-8 h-8 rounded-full border border-zinc-700 shrink-0"
- style={{ backgroundColor: colorHex }}
- />
- {/* Info */}
- <div className="flex-1 min-w-0">
- <div className="flex items-center gap-2">
- <span className="text-sm font-medium text-zinc-100 truncate">
- {spool.material}
- {spool.color_name && <span className="text-zinc-400 ml-1">- {spool.color_name}</span>}
- </span>
- </div>
- {spool.brand && (
- <span className="text-xs text-zinc-500 truncate block">{spool.brand}</span>
- )}
- </div>
- {/* Weight */}
- <div className="text-right shrink-0">
- <div className="text-sm text-zinc-300">{Math.round(remaining)}g</div>
- <div className="text-xs text-zinc-500">{t('spoolbuddy.spool.remaining', 'Remaining')}</div>
- </div>
- </div>
- {/* Fill bar */}
- <div className="w-full h-1 bg-zinc-700 rounded-full overflow-hidden mt-2">
- <div
- className="h-full rounded-full transition-all"
- style={{
- width: `${Math.min(100, remainingPct)}%`,
- backgroundColor: remainingPct > 50 ? '#22c55e' : remainingPct > 15 ? '#f59e0b' : '#ef4444',
- }}
- />
- </div>
- {/* Expanded details */}
- {expanded && (
- <div className="mt-3 pt-3 border-t border-zinc-700 grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
- <div className="flex justify-between">
- <span className="text-zinc-500">{t('spoolbuddy.spool.labelWeight', 'Label')}</span>
- <span className="text-zinc-400">{spool.label_weight}g</span>
- </div>
- <div className="flex justify-between">
- <span className="text-zinc-500">{t('spoolbuddy.spool.coreWeight', 'Core')}</span>
- <span className="text-zinc-400">{spool.core_weight}g</span>
- </div>
- <div className="flex justify-between">
- <span className="text-zinc-500">{t('spoolbuddy.spool.material', 'Material')}</span>
- <span className="text-zinc-400">{spool.material}{spool.subtype ? ` ${spool.subtype}` : ''}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-zinc-500">{t('spoolbuddy.spool.lastUsed', 'Last used')}</span>
- <span className="text-zinc-400">{formatDate(spool.last_used)}</span>
- </div>
- {spool.tag_uid && (
- <div className="col-span-2 flex justify-between">
- <span className="text-zinc-500">Tag</span>
- <span className="text-zinc-400 font-mono">{spool.tag_uid}</span>
- </div>
- )}
- </div>
- )}
- </button>
- );
- }
- export function SpoolBuddyInventoryPage() {
- const { t } = useTranslation();
- const [search, setSearch] = useState('');
- const { data: spools = [], isLoading } = useQuery({
- queryKey: ['inventory-spools'],
- queryFn: () => api.getSpools(false),
- });
- const filtered = useMemo(() => {
- if (!search.trim()) return spools;
- const q = search.toLowerCase();
- return spools.filter((s) =>
- s.material.toLowerCase().includes(q) ||
- s.brand?.toLowerCase().includes(q) ||
- s.color_name?.toLowerCase().includes(q)
- );
- }, [spools, search]);
- return (
- <div className="h-full flex flex-col p-4">
- {/* Header */}
- <div className="flex items-center justify-between mb-4">
- <h1 className="text-lg font-semibold text-zinc-100">
- {t('spoolbuddy.nav.inventory', 'Inventory')}
- </h1>
- <span className="text-sm text-zinc-500">{spools.length} {t('spoolbuddy.inventory.spools', 'spools')}</span>
- </div>
- {/* Search */}
- <div className="relative mb-4">
- <svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
- </svg>
- <input
- type="text"
- value={search}
- onChange={(e) => setSearch(e.target.value)}
- placeholder={t('spoolbuddy.inventory.search', 'Search spools...')}
- className="w-full pl-10 pr-4 py-2.5 bg-zinc-800 border border-zinc-700 rounded-lg text-sm text-zinc-100 placeholder-zinc-500 focus:outline-none focus:border-green-500 min-h-[44px]"
- />
- </div>
- {/* Spool list */}
- <div className="flex-1 min-h-0 overflow-y-auto space-y-2">
- {isLoading ? (
- <div className="flex items-center justify-center h-32">
- <span className="text-zinc-500">{t('common.loading', 'Loading...')}</span>
- </div>
- ) : filtered.length === 0 ? (
- <div className="flex flex-col items-center justify-center h-32 text-zinc-500">
- <svg className="w-10 h-10 mb-2 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
- </svg>
- <p className="text-sm">{search ? t('spoolbuddy.inventory.noResults', 'No matching spools') : t('spoolbuddy.inventory.empty', 'No spools in inventory')}</p>
- </div>
- ) : (
- filtered.map((spool) => <SpoolCard key={spool.id} spool={spool} />)
- )}
- </div>
- </div>
- );
- }
|