wip: in-progress feature work (manual entry, collection view)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,8 +55,8 @@ Requirements for this milestone. Each maps to roadmap phases.
|
|||||||
- [x] **CATFLOW-04**: Collection items referencing global items display merged data (global base + personal overlay)
|
- [x] **CATFLOW-04**: Collection items referencing global items display merged data (global base + personal overlay)
|
||||||
- [x] **CATFLOW-05**: Thread candidates can be added from catalog with global item link
|
- [x] **CATFLOW-05**: Thread candidates can be added from catalog with global item link
|
||||||
- [x] **CATFLOW-06**: Thread resolution with catalog-linked candidate creates reference item with auto-link
|
- [x] **CATFLOW-06**: Thread resolution with catalog-linked candidate creates reference item with auto-link
|
||||||
- [ ] **CATFLOW-07**: Manual entry fallback when item not in catalog
|
- [x] **CATFLOW-07**: Manual entry fallback when item not in catalog
|
||||||
- [ ] **CATFLOW-08**: Non-functional "Submit to catalog?" prompt shown after manual save
|
- [x] **CATFLOW-08**: Non-functional "Submit to catalog?" prompt shown after manual save
|
||||||
|
|
||||||
### Item & Catalog Detail Pages
|
### Item & Catalog Detail Pages
|
||||||
|
|
||||||
@@ -177,8 +177,8 @@ Which phases cover which requirements. Updated during roadmap creation.
|
|||||||
| CATFLOW-04 | Phase 19 | Complete |
|
| CATFLOW-04 | Phase 19 | Complete |
|
||||||
| CATFLOW-05 | Phase 19, 22 | Complete |
|
| CATFLOW-05 | Phase 19, 22 | Complete |
|
||||||
| CATFLOW-06 | Phase 19, 22 | Complete |
|
| CATFLOW-06 | Phase 19, 22 | Complete |
|
||||||
| CATFLOW-07 | Phase 23 | Pending |
|
| CATFLOW-07 | Phase 23 | Complete |
|
||||||
| CATFLOW-08 | Phase 23 | Pending |
|
| CATFLOW-08 | Phase 23 | Complete |
|
||||||
| TAG-01 | Phase 19 | Complete |
|
| TAG-01 | Phase 19 | Complete |
|
||||||
| TAG-02 | Phase 19 | Complete |
|
| TAG-02 | Phase 19 | Complete |
|
||||||
| DETAIL-01 | Phase 21 | Pending |
|
| DETAIL-01 | Phase 21 | Pending |
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"granularity": "coarse",
|
"granularity": "coarse",
|
||||||
"parallelization": true,
|
"parallelization": true,
|
||||||
"commit_docs": true,
|
"commit_docs": true,
|
||||||
"model_profile": "quality",
|
"model_profile": "balanced",
|
||||||
"workflow": {
|
"workflow": {
|
||||||
"research": true,
|
"research": true,
|
||||||
"plan_check": true,
|
"plan_check": true,
|
||||||
|
|||||||
16
CLAUDE.md
16
CLAUDE.md
@@ -86,12 +86,24 @@ curl -s -X POST "https://gitea.jeanlucmakiola.de/api/v1/repos/makiolaj/GearBox/a
|
|||||||
|
|
||||||
`@/*` maps to `./src/*` (configured in tsconfig.json).
|
`@/*` maps to `./src/*` (configured in tsconfig.json).
|
||||||
|
|
||||||
|
## Reusable Components
|
||||||
|
|
||||||
|
Always use existing components instead of rebuilding with plain HTML. Check `src/client/components/` before creating new form elements or UI patterns.
|
||||||
|
|
||||||
|
| Need | Use | Not |
|
||||||
|
|------|-----|-----|
|
||||||
|
| Category selection | `CategoryPicker` (icons, search, inline create) | Plain `<select>` |
|
||||||
|
| Icon selection | `IconPicker` (119 curated Lucide icons, grouped) | Manual icon input |
|
||||||
|
| Image upload | `ImageUpload` (preview, click-to-upload) | Raw file input |
|
||||||
|
| Lucide icon rendering | `LucideIcon` from `lib/iconData` | Direct SVG or lucide-react imports |
|
||||||
|
| Weight/price formatting | `useFormatters()` hook | Manual formatting |
|
||||||
|
|
||||||
## Key Patterns
|
## Key Patterns
|
||||||
|
|
||||||
- **Thread resolution**: Resolving a thread copies the winning candidate's data into a new item in the collection, sets `resolvedCandidateId`, and changes status to "resolved".
|
- **Thread resolution**: Resolving a thread copies the winning candidate's data into a new item in the collection, sets `resolvedCandidateId`, and changes status to "resolved".
|
||||||
- **Setup item sync**: `PUT /api/setups/:id/items` replaces all setup_items atomically (delete all, re-insert).
|
- **Setup item sync**: `PUT /api/setups/:id/items` replaces all setup_items atomically (delete all, re-insert).
|
||||||
- **Image uploads**: `POST /api/images` saves to `./uploads/` with UUID filename, returned as `imageFilename` on item/candidate records.
|
- **Image uploads**: `POST /api/images` saves to S3-compatible storage (Garage/R2), returned as `imageFilename` on item/candidate records.
|
||||||
- **Image URL fetching**: `POST /api/images/from-url` fetches an image from a URL, saves locally, returns `{ filename, sourceUrl }`.
|
- **Image URL fetching**: `POST /api/images/from-url` fetches an image from a URL, saves to S3, returns `{ filename, sourceUrl }`.
|
||||||
- **Aggregates** (weight/cost totals): Computed via SQL on read, not stored on records.
|
- **Aggregates** (weight/cost totals): Computed via SQL on read, not stored on records.
|
||||||
- **Authentication**: Public-read, authenticated-write. Cookie sessions for web UI, API keys (`X-API-Key` header) for programmatic access. `POST /api/auth/setup` for first-time account creation. Auth middleware protects all POST/PUT/DELETE on `/api/*` except `/api/auth/*`.
|
- **Authentication**: Public-read, authenticated-write. Cookie sessions for web UI, API keys (`X-API-Key` header) for programmatic access. `POST /api/auth/setup` for first-time account creation. Auth middleware protects all POST/PUT/DELETE on `/api/*` except `/api/auth/*`.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { toast } from "sonner";
|
|||||||
import { useCategories } from "../hooks/useCategories";
|
import { useCategories } from "../hooks/useCategories";
|
||||||
import { useCreateItem } from "../hooks/useItems";
|
import { useCreateItem } from "../hooks/useItems";
|
||||||
import { useUIStore } from "../stores/uiStore";
|
import { useUIStore } from "../stores/uiStore";
|
||||||
|
import { CategoryPicker } from "./CategoryPicker";
|
||||||
|
|
||||||
export function AddToCollectionModal() {
|
export function AddToCollectionModal() {
|
||||||
const { open, globalItemId, globalItemName } = useUIStore(
|
const { open, globalItemId, globalItemName } = useUIStore(
|
||||||
@@ -95,24 +96,13 @@ export function AddToCollectionModal() {
|
|||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
htmlFor="collection-category"
|
|
||||||
className="block text-sm font-medium text-gray-700 mb-1"
|
|
||||||
>
|
|
||||||
Category
|
Category
|
||||||
</label>
|
</label>
|
||||||
<select
|
<CategoryPicker
|
||||||
id="collection-category"
|
value={categoryId ?? 0}
|
||||||
value={categoryId ?? ""}
|
onChange={(id) => setCategoryId(id)}
|
||||||
onChange={(e) => setCategoryId(Number(e.target.value))}
|
/>
|
||||||
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent bg-white"
|
|
||||||
>
|
|
||||||
{categories?.map((cat) => (
|
|
||||||
<option key={cat.id} value={cat.id}>
|
|
||||||
{cat.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ export function CollectionView() {
|
|||||||
imageFilename={item.imageFilename}
|
imageFilename={item.imageFilename}
|
||||||
imageUrl={item.imageUrl}
|
imageUrl={item.imageUrl}
|
||||||
productUrl={item.productUrl}
|
productUrl={item.productUrl}
|
||||||
|
brand={item.brand}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -264,6 +265,7 @@ export function CollectionView() {
|
|||||||
categoryIcon={categoryIcon}
|
categoryIcon={categoryIcon}
|
||||||
imageFilename={item.imageFilename}
|
imageFilename={item.imageFilename}
|
||||||
productUrl={item.productUrl}
|
productUrl={item.productUrl}
|
||||||
|
brand={item.brand}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ interface ItemWithCategory {
|
|||||||
notes: string | null;
|
notes: string | null;
|
||||||
productUrl: string | null;
|
productUrl: string | null;
|
||||||
imageFilename: string | null;
|
imageFilename: string | null;
|
||||||
|
globalItemId: number | null;
|
||||||
|
brand: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ export const iconGroups: IconGroup[] = [
|
|||||||
// --- LucideIcon render component ---
|
// --- LucideIcon render component ---
|
||||||
|
|
||||||
function toPascalCase(str: string): string {
|
function toPascalCase(str: string): string {
|
||||||
|
if (!str) return "Package";
|
||||||
return str
|
return str
|
||||||
.split("-")
|
.split("-")
|
||||||
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ export async function getAllItems(db: Db, userId: number) {
|
|||||||
)`.as("image_filename"),
|
)`.as("image_filename"),
|
||||||
imageSourceUrl: items.imageSourceUrl,
|
imageSourceUrl: items.imageSourceUrl,
|
||||||
globalItemId: items.globalItemId,
|
globalItemId: items.globalItemId,
|
||||||
|
brand: sql<
|
||||||
|
string | null
|
||||||
|
>`COALESCE(${globalItems.brand}, ${items.brand})`.as("brand"),
|
||||||
createdAt: items.createdAt,
|
createdAt: items.createdAt,
|
||||||
updatedAt: items.updatedAt,
|
updatedAt: items.updatedAt,
|
||||||
categoryName: categories.name,
|
categoryName: categories.name,
|
||||||
@@ -66,6 +69,7 @@ export async function getItemById(db: Db, userId: number, id: number) {
|
|||||||
${items.priceCents}
|
${items.priceCents}
|
||||||
)`.as("price_cents"),
|
)`.as("price_cents"),
|
||||||
purchasePriceCents: items.purchasePriceCents,
|
purchasePriceCents: items.purchasePriceCents,
|
||||||
|
quantity: items.quantity,
|
||||||
categoryId: items.categoryId,
|
categoryId: items.categoryId,
|
||||||
notes: items.notes,
|
notes: items.notes,
|
||||||
productUrl: items.productUrl,
|
productUrl: items.productUrl,
|
||||||
@@ -75,10 +79,16 @@ export async function getItemById(db: Db, userId: number, id: number) {
|
|||||||
)`.as("image_filename"),
|
)`.as("image_filename"),
|
||||||
imageSourceUrl: items.imageSourceUrl,
|
imageSourceUrl: items.imageSourceUrl,
|
||||||
globalItemId: items.globalItemId,
|
globalItemId: items.globalItemId,
|
||||||
|
brand: sql<
|
||||||
|
string | null
|
||||||
|
>`COALESCE(${globalItems.brand}, ${items.brand})`.as("brand"),
|
||||||
createdAt: items.createdAt,
|
createdAt: items.createdAt,
|
||||||
updatedAt: items.updatedAt,
|
updatedAt: items.updatedAt,
|
||||||
|
categoryName: categories.name,
|
||||||
|
categoryIcon: categories.icon,
|
||||||
})
|
})
|
||||||
.from(items)
|
.from(items)
|
||||||
|
.innerJoin(categories, eq(items.categoryId, categories.id))
|
||||||
.leftJoin(globalItems, eq(items.globalItemId, globalItems.id))
|
.leftJoin(globalItems, eq(items.globalItemId, globalItems.id))
|
||||||
.where(and(eq(items.id, id), eq(items.userId, userId)));
|
.where(and(eq(items.id, id), eq(items.userId, userId)));
|
||||||
|
|
||||||
@@ -143,6 +153,7 @@ export async function updateItem(
|
|||||||
imageSourceUrl: string;
|
imageSourceUrl: string;
|
||||||
globalItemId: number;
|
globalItemId: number;
|
||||||
purchasePriceCents: number;
|
purchasePriceCents: number;
|
||||||
|
brand: string;
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
// Check if item exists and belongs to user
|
// Check if item exists and belongs to user
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const createItemSchema = z.object({
|
|||||||
quantity: z.number().int().positive().optional(),
|
quantity: z.number().int().positive().optional(),
|
||||||
globalItemId: z.number().int().positive().optional(),
|
globalItemId: z.number().int().positive().optional(),
|
||||||
purchasePriceCents: z.number().int().nonnegative().optional(),
|
purchasePriceCents: z.number().int().nonnegative().optional(),
|
||||||
|
brand: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateItemSchema = createItemSchema.partial().extend({
|
export const updateItemSchema = createItemSchema.partial().extend({
|
||||||
|
|||||||
Reference in New Issue
Block a user