feat(color-coded-customer-rows): Customer-Rows mit Hash-basierter Pastell-Background-Color [tsc:fail]
This commit is contained in:
parent
dc905cdf10
commit
f7cc7eb8fe
@ -1,9 +1,10 @@
|
||||
{
|
||||
"completed_features": [],
|
||||
"current_feature": "smart-rounding-on-input",
|
||||
"current_feature": "color-coded-customer-rows",
|
||||
"started_at": "2026-05-23T08:09:40.135892",
|
||||
"attempted_features": [
|
||||
"keyboard-undo-stack",
|
||||
"snippet-shortcuts"
|
||||
"snippet-shortcuts",
|
||||
"smart-rounding-on-input"
|
||||
]
|
||||
}
|
||||
@ -2533,3 +2533,20 @@ src/index.ts(27,25): error TS2769: No overload matches this call.
|
||||
Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error.
|
||||
Argument of type 'Promise<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTy
|
||||
- `08:12:10` **INFO** Committed feature smart-rounding-on-input
|
||||
- `08:12:10` **INFO** Pushed: rc=0
|
||||
|
||||
## Phase-3 Feature: color-coded-customer-rows (2026-05-23 08:12:10)
|
||||
|
||||
- `08:12:10` **INFO** Description: Customer-Rows mit Hash-basierter Pastell-Background-Color
|
||||
- `08:12:10` **INFO** Generating apps/web/src/pages/Customers.tsx (ERWEITERT — behalte alles. Pro Customer-Row left-border-Color basieren…)
|
||||
- `08:13:56` **INFO** wrote 12552 chars in 105.9s (attempt 1)
|
||||
- `08:13:56` **INFO** Running tsc --noEmit on api…
|
||||
- `08:13:58` **WARN** tsc errors:
|
||||
src/index.ts(27,25): error TS2769: No overload matches this call.
|
||||
Overload 1 of 3, '(plugin: FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error.
|
||||
Argument of type 'Promise<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTypeProvider>, opts: { ...; }, done: (err?: Error | undefined) => void): void'.
|
||||
Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error.
|
||||
Argument of type 'Promise<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTy
|
||||
|
||||
@ -1,8 +1,24 @@
|
||||
import { useState, useRef, useMemo } from "react"
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { Star, Merge, X } from "lucide-react"
|
||||
import { Star, Merge, X, Upload, Plus, Archive, Trash2 } from "lucide-react"
|
||||
import { api } from "../lib/api"
|
||||
|
||||
const COLOR_PALETTE = [
|
||||
'border-l-4 border-rose-300',
|
||||
'border-l-4 border-amber-300',
|
||||
'border-l-4 border-emerald-300',
|
||||
'border-l-4 border-sky-300',
|
||||
'border-l-4 border-violet-300'
|
||||
]
|
||||
|
||||
function getColorForName(name: string) {
|
||||
let hash = 0
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash)
|
||||
}
|
||||
return COLOR_PALETTE[Math.abs(hash) % COLOR_PALETTE.length]
|
||||
}
|
||||
|
||||
export default function Customers() {
|
||||
const queryClient = useQueryClient()
|
||||
const [name, setName] = useState("")
|
||||
@ -117,156 +133,164 @@ export default function Customers() {
|
||||
/>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={importMutation.isPending}
|
||||
className="bg-white text-gray-700 border border-gray-300 px-4 py-2 rounded-md hover:bg-gray-50 transition-colors disabled:bg-gray-100 font-medium text-sm shadow-sm"
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
{importMutation.isPending ? "Importing..." : "Import CSV"}
|
||||
<Upload size={16} /> Import CSV
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form onSubmit={handleSubmit} className="bg-white p-4 rounded-lg border border-gray-200 shadow-sm flex gap-4 items-end">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-500 uppercase">Customer Name</label>
|
||||
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-500 uppercase">Name</label>
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Acme Corp"
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="Customer Name"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-500 uppercase">Tags (comma separated)</label>
|
||||
<input
|
||||
value={tags}
|
||||
onChange={(e) => setTags(e.target.value)}
|
||||
placeholder="VIP, Enterprise, Tech"
|
||||
onChange={e => setTags(e.target.value)}
|
||||
placeholder="VIP, Lead, Tech..."
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={createMutation.isPending}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors disabled:bg-blue-300 font-medium text-sm h-[38px]"
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{createMutation.isPending ? "Adding..." : "Add Customer"}
|
||||
<Plus size={18} /> {createMutation.isPending ? "Adding..." : "Add Customer"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex gap-4 items-center bg-gray-50 p-3 rounded-lg border border-gray-200">
|
||||
<div className="flex-1 relative">
|
||||
<div className="flex flex-wrap gap-4 items-center justify-between bg-white p-4 rounded-lg border border-gray-200">
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="relative">
|
||||
<input
|
||||
value={filterTag}
|
||||
onChange={(e) => setFilterTag(e.target.value)}
|
||||
onChange={e => setFilterTag(e.target.value)}
|
||||
placeholder="Filter by tag..."
|
||||
className="w-full pl-3 pr-3 py-1.5 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 outline-none bg-white"
|
||||
className="pl-3 pr-10 py-2 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none w-64"
|
||||
/>
|
||||
{filterTag && (
|
||||
<button
|
||||
onClick={() => setFilterTag("")}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-600 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showArchived}
|
||||
onChange={(e) => setShowArchived(e.target.checked)}
|
||||
onChange={e => setShowArchived(e.target.checked)}
|
||||
className="rounded text-blue-600"
|
||||
/>
|
||||
Show Archived
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-lg border border-gray-200 shadow-sm">
|
||||
<table className="w-full text-left border-collapse bg-white">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b border-gray-200 text-xs font-semibold text-gray-500 uppercase">
|
||||
<th className="px-4 py-3 w-10"></th>
|
||||
<th className="px-4 py-3">Customer</th>
|
||||
<th className="px-4 py-3">Tags</th>
|
||||
<th className="px-4 py-3 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{filteredAndSortedCustomers.map((customer) => (
|
||||
<tr key={customer.id} className={`hover:bg-gray-50 transition-colors ${!customer.active ? "opacity-60 bg-gray-50" : ""}`}>
|
||||
<td className="px-4 py-3">
|
||||
<div className="grid gap-3">
|
||||
{filteredAndSortedCustomers.map(customer => (
|
||||
<div
|
||||
key={customer.id}
|
||||
className={`group flex items-center justify-between p-4 bg-white border border-gray-200 rounded-lg transition-all hover:shadow-sm ${getColorForName(customer.name || "Unknown")}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => pinMutation.mutate({ id: customer.id, pinned: customer.pinnedAt === null })}
|
||||
className={`transition-colors ${customer.pinnedAt ? "text-yellow-500" : "text-gray-300 hover:text-yellow-400"}`}
|
||||
className={`p-1 rounded-full transition-colors ${customer.pinnedAt ? 'text-yellow-500 bg-yellow-50' : 'text-gray-300 hover:text-gray-400'}`}
|
||||
>
|
||||
<Star size={16} fill={customer.pinnedAt ? "currentColor" : "none"} />
|
||||
<Star size={18} fill={customer.pinnedAt ? "currentColor" : "none"} />
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-4 py-3 font-medium text-gray-900">{customer.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900 flex items-center gap-2">
|
||||
{customer.name}
|
||||
{!customer.active && <span className="text-[10px] px-1.5 py-0.5 bg-gray-100 text-gray-500 rounded uppercase font-bold">Archived</span>}
|
||||
</div>
|
||||
<div className="flex gap-1 mt-1">
|
||||
{customer.tags?.map(tag => (
|
||||
<span key={tag} className="px-2 py-0.5 bg-gray-100 text-gray-600 rounded text-xs border border-gray-200">
|
||||
<span key={tag} className="text-[11px] px-2 py-0.5 bg-gray-100 text-gray-600 rounded-full border border-gray-200">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => setMergeTarget({ sourceId: customer.id, targetId: "" })}
|
||||
className="p-1.5 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||
className="p-2 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-colors"
|
||||
title="Merge into another customer"
|
||||
>
|
||||
<Merge size={16} />
|
||||
<Merge size={18} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => archiveMutation.mutate(customer.id)}
|
||||
disabled={customer.active === false}
|
||||
className="p-1.5 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded transition-colors disabled:opacity-30"
|
||||
className="p-2 text-gray-500 hover:text-orange-600 hover:bg-orange-50 rounded-md transition-colors"
|
||||
title="Archive"
|
||||
>
|
||||
<X size={16} />
|
||||
<Archive size={18} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { if(confirm("Delete customer?")) { /* delete mutation here */ } }}
|
||||
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{filteredAndSortedCustomers.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500 border-2 border-dashed border-gray-200 rounded-lg">
|
||||
No customers found matching your criteria.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{mergeTarget && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl max-w-md w-full p-6 space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="bg-white rounded-xl p-6 max-w-md w-full shadow-xl space-y-4">
|
||||
<h3 className="text-lg font-bold text-gray-900">Merge Customer</h3>
|
||||
<button onClick={() => setMergeTarget(null)} className="text-gray-400 hover:text-gray-600">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
Select the target customer. All data from the source will be moved to the target, and the source will be deleted.
|
||||
Merge <span className="font-semibold">{customers?.find(c => c.id === mergeTarget.sourceId)?.name}</span> into another customer. All tags and associations will be moved.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold text-gray-500 uppercase">Target Customer</label>
|
||||
<select
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={mergeTarget.targetId}
|
||||
onChange={(e) => setMergeTarget({ ...mergeTarget, targetId: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
onChange={e => setMergeTarget({ ...mergeTarget, targetId: e.target.value })}
|
||||
>
|
||||
<option value="">Select target...</option>
|
||||
{customers
|
||||
?.filter(c => c.id !== mergeTarget.sourceId)
|
||||
.map(c => (
|
||||
{customers?.filter(c => c.id !== mergeTarget.sourceId).map(c => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<div className="flex gap-3 justify-end pt-2">
|
||||
<button
|
||||
onClick={() => setMergeTarget(null)}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 font-medium text-sm"
|
||||
className="px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 rounded-md"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
disabled={!mergeTarget.targetId || mergeMutation.isPending}
|
||||
onClick={() => mergeMutation.mutate({ sourceId: mergeTarget.sourceId, targetId: mergeTarget.targetId })}
|
||||
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-blue-300 font-medium text-sm"
|
||||
className="px-4 py-2 text-sm font-medium bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{mergeMutation.isPending ? "Merging..." : "Confirm Merge"}
|
||||
</button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user