feat: Quiet Luxury design system + Pipeline (ex-Lead) redesign
- Design system foundations: Plus Jakarta Sans font, shadow-card/radius tokens in @theme, design-reference/DESIGN-SYSTEM.md with component inventory - Collapsible admin shell (AdminShell): smooth w-64<->w-20 sidebar, new top header with "Admin" + avatar placeholder, localStorage-persisted state - Rename route /admin/leads -> /admin/pipeline (redirect stubs preserved), nav label Lead -> Pipeline; DB table unchanged - Reusable primitives: StatusBadge, SearchInput, SegmentedToggle (dual-theme) - Luxury restyle of lead table, kanban (6 stages + @dnd-kit intact), PageHeader - Tokenize editable-cell / option-multi-select for dark-mode legibility Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,433 @@
|
|||||||
|
# Phase 19: Pipeline CRM Kanban — Research
|
||||||
|
|
||||||
|
**Researched:** 2026-06-19
|
||||||
|
**Domain:** @dnd-kit drag-drop, Next.js App Router client components, CRM leads view toggle
|
||||||
|
**Confidence:** HIGH — all findings verified directly from codebase
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<phase_requirements>
|
||||||
|
## Phase Requirements
|
||||||
|
|
||||||
|
| ID | Description | Research Support |
|
||||||
|
|----|-------------|------------------|
|
||||||
|
| PIPE-01 | I lead sono visualizzabili in una board Kanban stile Pipedrive con colonne per stage e drag-drop per cambiare stage | `leads.status` enum verified (6 stages); `@dnd-kit/core` v6.3.1 already installed; exact analog in `KanbanBoard.tsx` |
|
||||||
|
| PIPE-02 | Spostare un lead nelle colonne "Vinto"/"Perso" è il cambio-stato manuale dell'esito | `won`/`lost` are existing LEAD_STAGES values; `updateLeadField(id, "status", value)` handles this today via the table dropdown |
|
||||||
|
</phase_requirements>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Phase 14 delivered a complete inline-edit table view of leads (`LeadTable.tsx`) backed by a solid data layer: `getLeadsWithTags()`, `updateLeadField()`, typed `LEAD_STAGES`, and a polymorphic tag system. Phase 19 adds a second view — Kanban — toggled from the same page, without replacing or changing any of that.
|
||||||
|
|
||||||
|
The project already ships a working `KanbanBoard.tsx` (for project tasks) that uses exactly the `@dnd-kit` primitives needed here. The new `LeadsKanbanBoard` is a direct structural analog: swap task status columns (`todo/in_progress/done`) for lead stage columns (`contacted/qualified/proposal_sent/negotiating/won/lost`), swap task cards for lead cards, swap `updateTaskStatus` for `updateLeadField(id, "status", newStage)`.
|
||||||
|
|
||||||
|
The view-toggle pattern is also ready in `PhasesViewToggle.tsx` — a client component that holds `useState<"list" | "kanban">` and renders either `listView` (a `ReactNode` passed as prop) or the kanban. The leads page only needs a `LeadsViewToggle` wrapper that receives the existing `LeadsSearch` as the `listView` slot and the new `LeadsKanbanBoard` as the kanban.
|
||||||
|
|
||||||
|
No schema changes. No new server actions. No new dependencies. This is a pure UI addition.
|
||||||
|
|
||||||
|
**Primary recommendation:** Copy the `KanbanBoard.tsx` structure exactly; adapt for 6 lead-stage columns; wire to `updateLeadField`; wrap with a `LeadsViewToggle` component in `LeadsSearch` or at the page level.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architectural Responsibility Map
|
||||||
|
|
||||||
|
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||||||
|
|------------|-------------|----------------|-----------|
|
||||||
|
| Kanban board rendering + drag state | Browser / Client | — | Drag-drop is inherently client-side; `"use client"` required |
|
||||||
|
| Lead status persistence on drop | API / Backend (Server Action) | — | `updateLeadField` is already a `"use server"` action |
|
||||||
|
| Lead data fetching | Frontend Server (SSR) | — | `LeadsPage` is a server component; passes data down as props |
|
||||||
|
| View toggle state (table / kanban) | Browser / Client | — | `useState` in a client wrapper component |
|
||||||
|
| Column definitions (stage labels, colors) | Browser / Client | — | Derived from `LEAD_STAGES` constant, purely presentational |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Standard Stack
|
||||||
|
|
||||||
|
### Core (already installed — no new installs needed)
|
||||||
|
|
||||||
|
| Library | Version | Purpose | Why Standard |
|
||||||
|
|---------|---------|---------|--------------|
|
||||||
|
| @dnd-kit/core | ^6.3.1 [VERIFIED: package.json] | DndContext, useDraggable, useDroppable, sensors | Already used in KanbanBoard.tsx |
|
||||||
|
| @dnd-kit/sortable | ^10.0.0 [VERIFIED: package.json] | Available but NOT used by existing KanbanBoard | Not needed; existing pattern uses useDraggable + useDroppable directly |
|
||||||
|
| @dnd-kit/utilities | ^3.2.2 [VERIFIED: package.json] | CSS.Transform helper | Imported if transform style needed |
|
||||||
|
| React (useTransition, useState) | via Next.js 16 | Optimistic state + async server action bridging | Project pattern |
|
||||||
|
|
||||||
|
**Installation:** None required. All dependencies already present.
|
||||||
|
|
||||||
|
### Existing Primitives Used by KanbanBoard.tsx [VERIFIED: src/components/admin/kanban/KanbanBoard.tsx]
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
DndContext, // Root context — wraps the entire board
|
||||||
|
DragEndEvent, // Event type for onDragEnd handler
|
||||||
|
DragOverlay, // Ghost card rendered at cursor during drag
|
||||||
|
PointerSensor, // Mouse/touch activation
|
||||||
|
KeyboardSensor, // Accessibility
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
useDroppable, // Applied to column containers
|
||||||
|
useDraggable, // Applied to individual cards
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `@dnd-kit/sortable` / `SortableContext` / `useSortable` are NOT used. The existing pattern uses the lower-level `useDraggable` + `useDroppable` primitives, which is appropriate for cross-column drag (not intra-column reordering).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
### System Architecture Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
LeadsPage (server component)
|
||||||
|
├─ getLeadsWithTags() ──────────────────────────────► Postgres / leads + tags
|
||||||
|
├─ getLeadFieldOptions() ───────────────────────────► Postgres / tags
|
||||||
|
└─ renders LeadsViewToggle (client component)
|
||||||
|
├─ [view="list"] → LeadsSearch → LeadTable (existing, unchanged)
|
||||||
|
└─ [view="kanban"] → LeadsKanbanBoard (new)
|
||||||
|
├─ DndContext (onDragEnd → updateLeadField server action)
|
||||||
|
├─ DroppableColumn × 6 (one per LEAD_STAGES value)
|
||||||
|
└─ DraggableLeadCard × N (one per lead)
|
||||||
|
└─ useTransition + router.refresh() (after persist)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├─ components/admin/leads/
|
||||||
|
│ ├─ LeadTable.tsx # EXISTING — unchanged
|
||||||
|
│ └─ LeadsKanbanBoard.tsx # NEW — analogous to KanbanBoard.tsx
|
||||||
|
├─ app/admin/leads/
|
||||||
|
│ ├─ page.tsx # MODIFIED — wrap with LeadsViewToggle
|
||||||
|
│ ├─ LeadsSearch.tsx # MODIFIED — receives view toggle or replaced by LeadsViewToggle
|
||||||
|
│ └─ actions.ts # EXISTING — updateLeadField already handles status changes
|
||||||
|
```
|
||||||
|
|
||||||
|
The view toggle can live either at the page level (simpler) or inside `LeadsSearch` (keeps search state alive across views). Recommended: extract a `LeadsViewToggle` client wrapper at the page level (same pattern as `PhasesViewToggle`), passing `<LeadsSearch leads={leads} options={options} />` as the `listView` ReactNode and `<LeadsKanbanBoard leads={leads} />` as the kanban.
|
||||||
|
|
||||||
|
### Pattern 1: Column definition for 6 lead stages
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Source: VERIFIED from src/lib/lead-validators.ts + src/components/admin/leads/LeadTable.tsx
|
||||||
|
|
||||||
|
type LeadStage = "contacted" | "qualified" | "proposal_sent" | "negotiating" | "won" | "lost";
|
||||||
|
|
||||||
|
const LEAD_COLUMNS: {
|
||||||
|
id: LeadStage;
|
||||||
|
label: string;
|
||||||
|
headerClass: string;
|
||||||
|
dotClass: string;
|
||||||
|
}[] = [
|
||||||
|
{ id: "contacted", label: "Contattato", headerClass: "text-[#71717a]", dotClass: "bg-[#d4d4d8]" },
|
||||||
|
{ id: "qualified", label: "Qualificato", headerClass: "text-[#1A463C]", dotClass: "bg-purple-400" },
|
||||||
|
{ id: "proposal_sent", label: "Offerta inviata", headerClass: "text-amber-700", dotClass: "bg-amber-400" },
|
||||||
|
{ id: "negotiating", label: "Trattativa", headerClass: "text-orange-700", dotClass: "bg-orange-400" },
|
||||||
|
{ id: "won", label: "Vinto", headerClass: "text-green-700", dotClass: "bg-green-500" },
|
||||||
|
{ id: "lost", label: "Perso", headerClass: "text-red-700", dotClass: "bg-red-400" },
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Lead Kanban Board (adapted from KanbanBoard.tsx)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Source: VERIFIED structure from src/components/admin/kanban/KanbanBoard.tsx
|
||||||
|
// Key adaptation: replace taskStatuses/updateTaskStatus with leadStatuses/updateLeadField
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import {
|
||||||
|
DndContext, DragEndEvent, DragOverlay,
|
||||||
|
PointerSensor, KeyboardSensor, useSensor, useSensors,
|
||||||
|
useDroppable, useDraggable,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import { updateLeadField } from "@/app/admin/leads/actions";
|
||||||
|
import type { LeadWithTags } from "@/lib/admin-queries";
|
||||||
|
|
||||||
|
export function LeadsKanbanBoard({ leads }: { leads: LeadWithTags[] }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [, startTransition] = useTransition();
|
||||||
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
|
const [leadStatuses, setLeadStatuses] = useState<Record<string, LeadStage>>(
|
||||||
|
() => Object.fromEntries(leads.map((l) => [l.id, l.status as LeadStage]))
|
||||||
|
);
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||||
|
useSensor(KeyboardSensor)
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleDragEnd(event: DragEndEvent) {
|
||||||
|
const { active, over } = event;
|
||||||
|
setActiveId(null);
|
||||||
|
if (!over) return;
|
||||||
|
const leadId = active.id as string;
|
||||||
|
const newStage = over.id as LeadStage;
|
||||||
|
if (newStage === leadStatuses[leadId]) return;
|
||||||
|
// Optimistic update
|
||||||
|
setLeadStatuses((prev) => ({ ...prev, [leadId]: newStage }));
|
||||||
|
// Persist
|
||||||
|
startTransition(async () => {
|
||||||
|
await updateLeadField(leadId, "status", newStage);
|
||||||
|
router.refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... render DndContext with LEAD_COLUMNS mapped to DroppableColumn
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: View toggle (adapted from PhasesViewToggle.tsx)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Source: VERIFIED from src/components/admin/kanban/PhasesViewToggle.tsx
|
||||||
|
"use client";
|
||||||
|
import { useState, type ReactNode } from "react";
|
||||||
|
import { LeadsKanbanBoard } from "@/components/admin/leads/LeadsKanbanBoard";
|
||||||
|
import type { LeadWithTags, LeadFieldOptions } from "@/lib/admin-queries";
|
||||||
|
|
||||||
|
export function LeadsViewToggle({
|
||||||
|
listView,
|
||||||
|
leads,
|
||||||
|
}: {
|
||||||
|
listView: ReactNode;
|
||||||
|
leads: LeadWithTags[];
|
||||||
|
}) {
|
||||||
|
const [view, setView] = useState<"list" | "kanban">("list");
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Toggle buttons — same pill pattern as PhasesViewToggle */}
|
||||||
|
{view === "list" ? listView : <LeadsKanbanBoard leads={leads} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: Lead card content
|
||||||
|
|
||||||
|
Each Kanban card should show: `name` (primary), `company` (secondary/optional), `next_action` (hint text, optional). Avoid showing `email`/`phone`/`tags` on the card to keep it compact — these are available in the table view.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Fields available on LeadWithTags (VERIFIED: src/lib/admin-queries.ts line 890)
|
||||||
|
// Lead & { tags: string[] }
|
||||||
|
// Relevant for card: name, company, next_action, status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
- **Using `useSortable` / `SortableContext`:** The existing project pattern does NOT use these. They are for intra-column reordering. Use `useDraggable` + `useDroppable` to match the established `KanbanBoard.tsx` pattern.
|
||||||
|
- **Calling `router.refresh()` before `await updateLeadField`:** Always await the server action first, then refresh. The existing KanbanBoard does this correctly inside `startTransition`.
|
||||||
|
- **Dropping `react-hook-form` / Zod on drag-drop:** No form validation needed for a status change — `updateLeadField` already validates via `LEAD_STAGES.includes(value)` check.
|
||||||
|
- **Removing `LeadsSearch` / `LeadTable`:** PIPE-01 requires the table to remain as an alternative view. Do not replace it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Don't Hand-Roll
|
||||||
|
|
||||||
|
| Problem | Don't Build | Use Instead | Why |
|
||||||
|
|---------|-------------|-------------|-----|
|
||||||
|
| Drag detection (distance threshold) | Custom mouse event tracking | `PointerSensor` with `activationConstraint: { distance: 5 }` | Already proven in KanbanBoard.tsx; prevents accidental drag on click |
|
||||||
|
| Keyboard accessibility for drag | Custom key handlers | `KeyboardSensor` from @dnd-kit/core | a11y for free |
|
||||||
|
| Drag ghost/overlay | CSS clone positioning | `DragOverlay` from @dnd-kit/core | Correct portal rendering, no z-index fights |
|
||||||
|
| Optimistic UI update | Complex local state with rollback | `useState` + `useTransition` (React pattern) | Already used in KanbanBoard.tsx and LeadTable.tsx |
|
||||||
|
| Status validation | Re-implementing LEAD_STAGES check | `updateLeadField` server action already validates status | DRY — the action throws on invalid stage |
|
||||||
|
|
||||||
|
**Key insight:** The entire drag-drop + persist pattern is already implemented and tested in `KanbanBoard.tsx`. This phase is a structural copy with domain adaptation, not a new implementation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Pitfall 1: Columns wider than viewport on 6-stage board
|
||||||
|
**What goes wrong:** 6 columns in `grid-cols-6` become too narrow on typical laptop screens (1280–1440px). The 3-column project kanban uses `grid-cols-3` with comfortable card width.
|
||||||
|
**Why it happens:** 6 × min-width ≈ 720px+ is tight.
|
||||||
|
**How to avoid:** Use `grid-cols-3 lg:grid-cols-6` or a horizontally scrollable container (`overflow-x-auto` on the grid wrapper). Alternatively, `min-w-[200px]` per column inside a scroll container.
|
||||||
|
**Warning signs:** Cards truncate before the lead name is visible.
|
||||||
|
|
||||||
|
### Pitfall 2: Won/Lost columns need visual distinction
|
||||||
|
**What goes wrong:** Dropping to "won" or "lost" looks identical to other columns — user may not notice the semantic weight of these terminal states.
|
||||||
|
**Why it happens:** Uniform column styling.
|
||||||
|
**How to avoid:** Use visually distinct `headerClass` (green for won, red for lost) and consider a stronger `isOver` highlight for these columns. The STAGE_COLOR map in `LeadTable.tsx` already defines these colors — reuse them.
|
||||||
|
|
||||||
|
### Pitfall 3: Leads not sorted consistently between views
|
||||||
|
**What goes wrong:** Table shows leads ordered by `updated_at DESC`; kanban derived from the same array shows different visual order depending on column grouping.
|
||||||
|
**Why it happens:** No explicit sort on the kanban card order within a column.
|
||||||
|
**How to avoid:** Sort leads within each column by `updated_at DESC` (same as the existing query order). The `getLeadsWithTags` query already returns `orderBy(desc(leads.updated_at))` so inheriting that order is sufficient.
|
||||||
|
|
||||||
|
### Pitfall 4: `router.refresh()` causes full re-mount of kanban
|
||||||
|
**What goes wrong:** After a drag-drop, `router.refresh()` rehydrates the server component, re-running `getLeadsWithTags()`. If the drag animation hasn't completed, it can cause a visual flicker.
|
||||||
|
**Why it happens:** Next.js App Router refresh re-renders the whole tree.
|
||||||
|
**How to avoid:** The existing `KanbanBoard.tsx` uses the same pattern without issue. The `setActiveId(null)` call in `handleDragEnd` clears the overlay before the refresh arrives, so the flicker is acceptable. This is the project's established pattern — do not deviate.
|
||||||
|
|
||||||
|
### Pitfall 5: Search/filter not available in kanban view
|
||||||
|
**What goes wrong:** The search bar lives in `LeadsSearch.tsx` and only filters `LeadTable`. If the user switches to kanban, they lose the ability to filter.
|
||||||
|
**Why it happens:** The view toggle renders either `LeadsSearch` (with its internal state) or the bare `LeadsKanbanBoard`.
|
||||||
|
**How to avoid:** Two acceptable approaches: (a) wrap both views together inside `LeadsSearch` and pass filtered leads to both (preferred — search state persists across view switches); or (b) accept that kanban shows all leads unfiltered (simpler, acceptable for now given the single-user context). Document the choice in the plan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
### Existing updateLeadField signature (server action)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Source: VERIFIED from src/app/admin/leads/actions.ts line 174
|
||||||
|
// EDITABLE_FIELDS includes "status" — drag-drop can call this directly
|
||||||
|
export async function updateLeadField(
|
||||||
|
leadId: string,
|
||||||
|
fieldName: "name" | "email" | "phone" | "company" | "status" | "next_action",
|
||||||
|
value: string
|
||||||
|
): Promise<void>
|
||||||
|
// Validates: status must be in LEAD_STAGES; throws on invalid value
|
||||||
|
// Side effects: revalidatePath("/admin/leads") + revalidatePath(`/admin/leads/${leadId}`)
|
||||||
|
```
|
||||||
|
|
||||||
|
### LEAD_STAGES canonical values
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Source: VERIFIED from src/lib/lead-validators.ts line 4
|
||||||
|
export const LEAD_STAGES = [
|
||||||
|
"contacted",
|
||||||
|
"qualified",
|
||||||
|
"proposal_sent",
|
||||||
|
"negotiating",
|
||||||
|
"won",
|
||||||
|
"lost",
|
||||||
|
] as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Existing STAGE_COLOR map (reuse for kanban column headers)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Source: VERIFIED from src/components/admin/leads/LeadTable.tsx line 20
|
||||||
|
const STAGE_COLOR: Record<string, string> = {
|
||||||
|
contacted: "bg-blue-100 text-blue-800",
|
||||||
|
qualified: "bg-purple-100 text-purple-800",
|
||||||
|
proposal_sent: "bg-amber-100 text-amber-800",
|
||||||
|
negotiating: "bg-orange-100 text-orange-800",
|
||||||
|
won: "bg-green-100 text-green-800",
|
||||||
|
lost: "bg-red-100 text-red-800",
|
||||||
|
};
|
||||||
|
// Move to a shared constant (e.g., src/lib/lead-constants.ts) if reused in both components
|
||||||
|
```
|
||||||
|
|
||||||
|
### PhasesViewToggle pattern (exact analog)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Source: VERIFIED from src/components/admin/kanban/PhasesViewToggle.tsx
|
||||||
|
// State: useState<"list" | "kanban">("list")
|
||||||
|
// Toggle: pill button group (bg-[#f4f4f5] rounded-lg p-1 w-fit)
|
||||||
|
// Active: bg-white text-[#1A463C] shadow-sm
|
||||||
|
// Inactive: text-[#71717a] hover:text-[#1a1a1a]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State of the Art
|
||||||
|
|
||||||
|
| Old Approach | Current Approach | When Changed | Impact |
|
||||||
|
|--------------|------------------|--------------|--------|
|
||||||
|
| Lead status change via modal form | Inline dropdown in table cell (`StatusCell`) | Phase 14 | Drag-drop is the third mechanism; all write to same `updateLeadField` action |
|
||||||
|
| Separate `/admin/analytics` route | Fused into `/admin` dashboard | Phase 18 | No impact on leads page |
|
||||||
|
| `SendQuoteModal` with dead branch | Dead branch removed | Phase 18 | No impact |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Constraints (from CLAUDE.md)
|
||||||
|
|
||||||
|
| Directive | Impact on This Phase |
|
||||||
|
|-----------|---------------------|
|
||||||
|
| `clients.token` = rotatable, never PK | Not relevant (leads have no token) |
|
||||||
|
| `quote_items` never exposed via client API | Not relevant (Kanban is admin-only) |
|
||||||
|
| `deliverables.approved_at` immutable once set | Not relevant |
|
||||||
|
| Auth: `/admin/*` → Auth.js session | Kanban lives at `/admin/leads` — already protected |
|
||||||
|
| No file hosting v1 | Not relevant |
|
||||||
|
| Migration safety: never drop/truncate rows | Phase 19 is UI-only — no schema changes, no migration needed |
|
||||||
|
| Security: confirm before destructive commands | No destructive operations |
|
||||||
|
| No package installs without showing name+version | No new packages needed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Availability
|
||||||
|
|
||||||
|
Step 2.6: SKIPPED — Phase 19 is a pure UI addition. All required libraries (`@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`) are already installed. No external services, databases (beyond the existing Neon Postgres connection), or CLI tools are needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Architecture
|
||||||
|
|
||||||
|
`nyquist_validation: false` in `.planning/config.json` — section omitted per config.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Domain
|
||||||
|
|
||||||
|
Phase 19 adds a new interaction path to an existing admin-only route (`/admin/leads`). No new auth surface is introduced.
|
||||||
|
|
||||||
|
| ASVS Category | Applies | Standard Control |
|
||||||
|
|---------------|---------|-----------------|
|
||||||
|
| V2 Authentication | yes (existing) | Auth.js session via `requireAdmin()` in server action |
|
||||||
|
| V4 Access Control | yes (existing) | `requireAdmin()` guard in `updateLeadField` — drag-drop calls same action |
|
||||||
|
| V5 Input Validation | yes | `updateLeadField` validates status via `LEAD_STAGES.includes(value)` — no new validation needed |
|
||||||
|
|
||||||
|
No new threat surface beyond what Phase 14 already addressed. The drag-drop `handleDragEnd` validates the `over.id` is a known stage before calling the server action — follow the same guard pattern as in `KanbanBoard.tsx` (line 190: `if (!(["todo", "in_progress", "done"] as string[]).includes(newStatus)) return;`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Assumptions Log
|
||||||
|
|
||||||
|
| # | Claim | Section | Risk if Wrong |
|
||||||
|
|---|-------|---------|---------------|
|
||||||
|
| A1 | The `won` and `lost` columns are terminal states with no special side-effects beyond setting `leads.status` (no auto-creation of client/project, no email trigger) | Architecture Patterns | If the plan later requires auto-provisioning on "won", a new server action will be needed — but PIPE-01/02 say nothing about this, and PROP-04 (auto-provisioning) is deferred to backlog post-R5 |
|
||||||
|
| A2 | Card content (name, company, next_action) is sufficient for the kanban view; no additional fields are needed per card | Code Examples | If the user wants tags or email visible on cards, the `LeadWithTags` type already provides them — no data-layer change, only card template change |
|
||||||
|
| A3 | The search filter covering only the table view (not the kanban) is acceptable for v1 of this feature | Common Pitfalls | If the user wants search in kanban too, the fix is to lift filtered state into the toggle wrapper — straightforward but adds scope |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **Search/filter scope in kanban view**
|
||||||
|
- What we know: `LeadsSearch` holds the search `useState` and passes `filtered` leads to `LeadTable`. The kanban would receive all leads from the page.
|
||||||
|
- What's unclear: Does the user want the search bar to filter the kanban board too, or is it acceptable that kanban shows all leads?
|
||||||
|
- Recommendation: Default to wrapping both views inside a new `LeadsViewToggle` that receives `leads` (unfiltered) and `options`, manages the view toggle, and passes `filtered` leads to both `LeadTable` and `LeadsKanbanBoard`. This is a clean pattern and handles it gracefully.
|
||||||
|
|
||||||
|
2. **Column layout: scroll vs. wrap on 6 columns**
|
||||||
|
- What we know: The existing kanban uses `grid-cols-3`. Six columns need more space.
|
||||||
|
- What's unclear: Target viewport is unknown (likely 1440px+ since this is a single-admin tool).
|
||||||
|
- Recommendation: Use `min-w-[180px]` per column inside an `overflow-x-auto` wrapper. This makes it work on any viewport without content truncation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
### Primary (HIGH confidence)
|
||||||
|
- `src/components/admin/kanban/KanbanBoard.tsx` — exact @dnd-kit usage pattern, drag primitives, sensors, DragOverlay, optimistic update + router.refresh()
|
||||||
|
- `src/components/admin/kanban/PhasesViewToggle.tsx` — view toggle pattern (list/kanban state, pill button UI)
|
||||||
|
- `src/components/admin/leads/LeadTable.tsx` — STAGE_COLOR map, LeadWithTags usage, StatusCell inline dropdown
|
||||||
|
- `src/app/admin/leads/actions.ts` — `updateLeadField` signature, EDITABLE_FIELDS, `requireAdmin()` guard
|
||||||
|
- `src/lib/lead-validators.ts` — canonical LEAD_STAGES array (6 values)
|
||||||
|
- `src/lib/admin-queries.ts` lines 883–942 — `LeadWithTags` type, `getLeadsWithTags()` query (all fields), `LeadFieldOptions`
|
||||||
|
- `src/db/schema.ts` lines 441–462 — `leads` table definition, `status` column with all 6 stage values documented
|
||||||
|
- `src/app/admin/leads/LeadsSearch.tsx` — search filter pattern, `LeadWithTags` + `LeadFieldOptions` prop interface
|
||||||
|
- `src/app/admin/leads/page.tsx` — server component structure, data fetching pattern, `revalidate = 0`
|
||||||
|
- `src/components/admin/AdminSidebar.tsx` — `/admin/leads` is already in NAV_ITEMS, no sidebar change needed
|
||||||
|
- `package.json` — @dnd-kit/core ^6.3.1, @dnd-kit/sortable ^10.0.0, @dnd-kit/utilities ^3.2.2
|
||||||
|
|
||||||
|
### Secondary (MEDIUM confidence)
|
||||||
|
- `.planning/config.json` — `nyquist_validation: false` confirmed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
**Confidence breakdown:**
|
||||||
|
- Standard stack: HIGH — all packages verified in package.json; exact primitives verified in KanbanBoard.tsx
|
||||||
|
- Architecture: HIGH — all patterns verified from existing codebase analogs
|
||||||
|
- Pitfalls: HIGH — derived from direct code inspection and known Next.js App Router behaviors
|
||||||
|
- Data layer: HIGH — schema, actions, and query functions all read directly
|
||||||
|
|
||||||
|
**Research date:** 2026-06-19
|
||||||
|
**Valid until:** Stable indefinitely (no external dependencies; codebase-derived findings)
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Design System: iamcavalli Admin & Client Portal (v1.0)
|
||||||
|
|
||||||
|
> "Minimalist Premium / Quiet Luxury" — the aesthetic that replaces the old
|
||||||
|
> mixed token/hardcoded styling across ClientHub's admin area. First applied
|
||||||
|
> to the Lead Pipeline page (Phase: Pipeline redesign); the tokens and
|
||||||
|
> primitives documented here are the base for restyling every other section.
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
- **Quiet, not loud.** Elevation is nearly invisible (shadow opacity ≤ 0.02),
|
||||||
|
borders are hairline, color is used sparingly and only to carry meaning
|
||||||
|
(status, semantics) — never for decoration.
|
||||||
|
- **Density with air.** Generous padding (`py-4 px-6` in tables, `p-4` in
|
||||||
|
kanban columns) paired with small type (`text-xs`, `text-[11px]`) reads as
|
||||||
|
premium rather than cramped.
|
||||||
|
- **Numbers are monospace.** Prices, phone numbers, dates, counts — anything
|
||||||
|
tabular/numeric — use `font-mono` and right-alignment so columns of digits
|
||||||
|
line up.
|
||||||
|
- **Dual-theme by construction.** Every surface is built from semantic
|
||||||
|
tokens (`bg-card`, `text-muted-foreground`, `border-border`, …), never raw
|
||||||
|
Tailwind palette classes (`bg-white`, `text-slate-900`, …) or hex literals.
|
||||||
|
This is what makes dark mode "just work" without a parallel dark stylesheet.
|
||||||
|
- **Motion is functional.** Transitions exist to explain state change (sidebar
|
||||||
|
collapse, hover, drag) — `duration-200`–`duration-350`,
|
||||||
|
`ease-[cubic-bezier(0.4,0,0.2,1)]` for the sidebar specifically. No
|
||||||
|
decorative animation.
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
|
||||||
|
- **Sans**: Plus Jakarta Sans (weights 300–700), loaded via `next/font/google`
|
||||||
|
as `--font-plus-jakarta-sans`, mapped to Tailwind's `--font-sans` in
|
||||||
|
`@theme`. Used for all UI text.
|
||||||
|
- **Mono**: Geist Mono, `--font-geist-mono` → `--font-mono`. Used for numeric
|
||||||
|
data cells only (prices, phone numbers, counts, dates in tables).
|
||||||
|
- **Scale conventions**:
|
||||||
|
- Page title: `text-2xl font-semibold tracking-tight`
|
||||||
|
- Page subtitle: `text-xs text-muted-foreground`
|
||||||
|
- Table header cells: `text-[11px] font-semibold uppercase tracking-wider text-muted-foreground`
|
||||||
|
- Table body: `text-sm`
|
||||||
|
- Badge/pill label: `text-[10px]`–`text-xs font-semibold uppercase tracking-wide`
|
||||||
|
|
||||||
|
## Color Tokens
|
||||||
|
|
||||||
|
Raw values live in `:root` (light) / `.dark` (dark) in `src/app/globals.css`
|
||||||
|
and are mapped to Tailwind utilities via `@theme inline`. Components must
|
||||||
|
consume the mapped utility, never the raw hex or a Tailwind palette shade.
|
||||||
|
|
||||||
|
| Utility | Light | Dark | Usage |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `bg-background` / `text-foreground` | `#ffffff` / `#1a1a1a` | `#0e1512` / `#f2f4f3` | Page canvas |
|
||||||
|
| `bg-card` / `text-card-foreground` | `#ffffff` | `#131a16` | Cards, table, kanban cards, header bar |
|
||||||
|
| `bg-muted` / `text-muted-foreground` | `#f9f9f9` / `#71717a` | `#171f1b` / `#9aa39e` | Subtle backgrounds, secondary text, kanban columns |
|
||||||
|
| `bg-primary` / `text-primary-foreground` | `#1A463C` | `#3FA88C` | Primary actions, sidebar, active nav state |
|
||||||
|
| `bg-accent` | `#DEF168` | `#DEF168` | Rare highlight accent (lime) |
|
||||||
|
| `border-border` / `border-input` | `#e5e7eb` | `#26302b` | All hairline borders |
|
||||||
|
| `ring-ring` | `#1A463C` | `#3FA88C` | Focus rings |
|
||||||
|
| `bg-destructive` | `#ef4444` | `#f87171` | Destructive actions |
|
||||||
|
|
||||||
|
**Sidebar exception**: the sidebar stays brand green `bg-[#1A463C]` in both
|
||||||
|
themes (it is not a themed surface — it's the constant brand anchor).
|
||||||
|
|
||||||
|
**Status/semantic colors** (lead stages, badges) use Tailwind's default
|
||||||
|
palette (blue/purple/amber/orange/emerald/red) directly, each paired with an
|
||||||
|
explicit `dark:` variant for legibility on dark surfaces — see `StatusBadge`.
|
||||||
|
|
||||||
|
## Elevation & Radius
|
||||||
|
|
||||||
|
- `--radius: 0.5rem` (base) is wired into `@theme` as `--radius-lg` (`=
|
||||||
|
--radius`) and `--radius-xl` (`= --radius + 0.25rem`), so `rounded-lg` /
|
||||||
|
`rounded-xl` utilities resolve consistently off one token.
|
||||||
|
- `--shadow-card: 0 4px 20px rgba(0,0,0,0.01)` → `shadow-card` utility.
|
||||||
|
Default resting elevation for cards, table containers, kanban cards.
|
||||||
|
- `--shadow-card-hover: 0 8px 30px rgba(0,0,0,0.02)` → `shadow-card-hover`
|
||||||
|
utility. Applied on hover for interactive cards.
|
||||||
|
|
||||||
|
## Layout Conventions
|
||||||
|
|
||||||
|
- **Table container**: `bg-card rounded-xl border border-border shadow-card
|
||||||
|
overflow-hidden`.
|
||||||
|
- **Table header row**: `bg-muted/50 border-b border-border text-[11px]
|
||||||
|
uppercase tracking-wider text-muted-foreground`.
|
||||||
|
- **Table body rows**: `py-4 px-6`, `divide-y divide-border`,
|
||||||
|
`hover:bg-muted/40 transition-colors`.
|
||||||
|
- **Kanban column**: `bg-muted/60 rounded-xl border border-border p-4`.
|
||||||
|
- **Kanban card**: `bg-card rounded-lg border border-border shadow-sm
|
||||||
|
hover:border-primary/30 hover:shadow-card-hover transition-all`.
|
||||||
|
- **Page header**: title `text-2xl font-semibold tracking-tight
|
||||||
|
text-foreground`, subtitle `text-xs text-muted-foreground mt-1`, primary
|
||||||
|
action button `bg-primary text-primary-foreground hover:bg-primary/90`.
|
||||||
|
|
||||||
|
## Color/Class Migration Map (dual-theme principle)
|
||||||
|
|
||||||
|
When restyling any surface, replace hardcoded classes with token utilities:
|
||||||
|
|
||||||
|
| Old (hardcoded) | New (token) |
|
||||||
|
|---|---|
|
||||||
|
| `bg-white` | `bg-card` |
|
||||||
|
| `bg-slate-50` / `bg-gray-50` | `bg-muted` |
|
||||||
|
| `border-slate-100` / `border-slate-200` | `border-border` |
|
||||||
|
| `text-slate-900` / `text-[#1a1a1a]` | `text-foreground` |
|
||||||
|
| `text-slate-500` / `text-slate-400` / `text-[#71717a]` | `text-muted-foreground` |
|
||||||
|
| `bg-brand-dark` / `text-[#1A463C]` (as action color) | `bg-primary` / `text-primary` |
|
||||||
|
|
||||||
|
## Motion
|
||||||
|
|
||||||
|
- **Sidebar collapse**: `transition-[width] duration-350
|
||||||
|
ease-[cubic-bezier(0.4,0,0.2,1)]` on the `<aside>`; label/logo text use
|
||||||
|
`whitespace-nowrap transition-opacity duration-200` and fade to
|
||||||
|
`opacity-0` immediately on collapse, but only fade back in ~150ms after
|
||||||
|
the width transition starts on expand (avoids text reflow/wrap during the
|
||||||
|
animation).
|
||||||
|
- **Hover states**: `transition-colors duration-150`–`duration-200` on rows,
|
||||||
|
buttons, nav links.
|
||||||
|
- **Kanban drag**: dragged card renders in a `DragOverlay` with
|
||||||
|
`rotate-1 shadow-xl`; drop target column highlights via `border-primary
|
||||||
|
bg-primary/5`.
|
||||||
|
|
||||||
|
## UX Rules
|
||||||
|
|
||||||
|
1. Numeric/tabular data is always monospace and right-aligned in tables.
|
||||||
|
2. Every interactive control has a visible focus state (`focus:ring-1
|
||||||
|
focus:ring-ring focus:border-primary` pattern) for keyboard accessibility.
|
||||||
|
3. Status is always communicated redundantly — color + text label — never
|
||||||
|
color alone.
|
||||||
|
4. Destructive actions use `destructive` tokens, never ad-hoc red.
|
||||||
|
5. Empty states are muted, centered, and brief ("Nessun lead trovato").
|
||||||
|
6. Sidebar collapse state persists across page loads via `localStorage`
|
||||||
|
(`iamcavalli:sidebar`) and is restored before paint where possible to
|
||||||
|
avoid layout jump.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Component Inventory
|
||||||
|
|
||||||
|
Primitives extracted from the Lead Pipeline redesign, intended for reuse
|
||||||
|
across all future admin pages.
|
||||||
|
|
||||||
|
| Component | Path | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `AdminShell` | `src/components/admin/AdminShell.tsx` | Client shell holding sidebar-collapsed state (persisted to `localStorage`), renders sidebar + top header + `<main>`. Wraps all `/admin/*` pages. |
|
||||||
|
| `AdminSidebar` | `src/components/admin/AdminSidebar.tsx` | Collapsible brand sidebar (`w-64` ↔ `w-20`), controlled by `AdminShell`. Nav items, theme toggle, sign-out. |
|
||||||
|
| `StatusBadge` | `src/components/ui/StatusBadge.tsx` | Rounded-full status pill with a color map per lead stage, each with a `dark:` variant. Replaces the old scattered `STAGE_COLOR` maps. |
|
||||||
|
| `SearchInput` | `src/components/ui/SearchInput.tsx` | Search-icon input, `rounded-lg border-border focus:ring-primary`. |
|
||||||
|
| `SegmentedToggle` | `src/components/ui/SegmentedToggle.tsx` | Generic Lista/Kanban-style segmented control (`bg-muted p-1 rounded-lg`, active option `bg-card shadow-sm`). |
|
||||||
|
| `PageHeader` | `src/components/admin/PageHeader.tsx` | Page title (`text-2xl tracking-tight`) + subtitle + action slot, token-based. |
|
||||||
|
| `LeadTable` | `src/components/admin/leads/LeadTable.tsx` | Luxury table restyle: `bg-card rounded-xl shadow-card`, uppercase muted headers, `hover:bg-muted/40` rows, uses `StatusBadge`. |
|
||||||
|
| `LeadsKanbanBoard` | `src/components/admin/leads/LeadsKanbanBoard.tsx` | Kanban restyle: `bg-muted/60` columns with count pills, `bg-card` cards with `hover:border-primary/30`. Uses `@dnd-kit`, keeps all 6 lead stages. |
|
||||||
|
| `LeadsViewToggle` | `src/components/admin/leads/LeadsViewToggle.tsx` | Wires `SearchInput` + `SegmentedToggle` together with the table/kanban views. |
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Catalogo Servizi — Luxury Admin CRM</title>
|
||||||
|
<!-- Google Fonts: Plus Jakarta Sans -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<!-- Tailwind CSS CDN -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Plus Jakarta Sans', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: {
|
||||||
|
dark: '#1A463C', /* Verde scuro richiesto */
|
||||||
|
darkHover: '#13342D', /* Variante scura per hover */
|
||||||
|
active: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
bg: '#F8F9FA',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.sidebar-transition {
|
||||||
|
transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1), transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-brand-bg font-sans text-slate-800 antialiased min-h-screen flex overflow-x-hidden">
|
||||||
|
|
||||||
|
<!-- SIDEBAR -->
|
||||||
|
<aside id="sidebar" class="w-64 bg-brand-dark text-white flex flex-col justify-between p-6 border-r border-emerald-950/20 shrink-0 sidebar-transition relative z-10">
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<!-- Logo e Intestazione -->
|
||||||
|
<div class="mb-10 px-2 flex items-center justify-between">
|
||||||
|
<span id="sidebar-logo-text" class="text-lg font-bold tracking-wider text-emerald-50 whitespace-nowrap transition-opacity duration-300">iamcavalli</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigazione Principale -->
|
||||||
|
<nav class="space-y-1">
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2v-4zM14 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2v-4z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Lead</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Clienti</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Progetti</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Preventivi</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M7 7h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Offerte</span>
|
||||||
|
</a>
|
||||||
|
<!-- Stato Attivo su Catalogo -->
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-white bg-brand-active font-medium transition-all duration-200 shadow-sm">
|
||||||
|
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Catalogo</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Area Inferiore Sidebar -->
|
||||||
|
<div class="border-t border-white/10 pt-4 space-y-3 overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between px-3 py-1 text-xs text-emerald-200/50 whitespace-nowrap">
|
||||||
|
<span class="sidebar-text transition-opacity duration-300">Tema</span>
|
||||||
|
<button class="p-1 rounded-full bg-white/5 hover:bg-white/10 text-emerald-200 hover:text-white transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707M14 12a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-red-300/80 hover:text-red-200 hover:bg-red-950/20 transition-all duration-200 whitespace-nowrap">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
|
||||||
|
<span class="sidebar-text transition-opacity duration-300">Esci</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- SEZIONE PRINCIPALE -->
|
||||||
|
<div class="flex-1 flex flex-col min-w-0">
|
||||||
|
|
||||||
|
<!-- BARRA DI INTESTAZIONE SUPERIORE -->
|
||||||
|
<header class="h-16 border-b border-slate-100 bg-white px-8 flex items-center justify-between shrink-0">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<!-- Pulsante Apri/Chiudi Sidebar -->
|
||||||
|
<button id="sidebar-toggle" class="p-2 -ml-2 rounded-lg text-slate-500 hover:bg-slate-50 hover:text-slate-800 transition-colors focus:outline-none" title="Espandi/Comprimi Sidebar">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 6h16M4 12h12M4 18h16"/></svg>
|
||||||
|
</button>
|
||||||
|
<span class="text-xs text-slate-400 font-medium">Area di Lavoro</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="text-xs font-semibold text-slate-700 bg-slate-100 px-2.5 py-1 rounded-full">Demo Account</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- AREA DEL CONTENUTO -->
|
||||||
|
<main class="flex-1 p-8 lg:p-10 max-w-[1400px] w-full mx-auto flex flex-col gap-6 overflow-y-auto">
|
||||||
|
|
||||||
|
<!-- INTESTAZIONE SEZIONE -->
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-semibold text-slate-900 tracking-tight">Catalogo Servizi</h1>
|
||||||
|
<p class="text-xs text-slate-400 mt-1">Configura e gestisci le singole voci di servizio offerte</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BARRA DI RICERCA MINIMALE -->
|
||||||
|
<div class="relative w-full">
|
||||||
|
<span class="absolute inset-y-0 left-0 flex items-center pl-3.5 pointer-events-none text-slate-400">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Cerca per nome, categoria, fase, tag o pacchetto..."
|
||||||
|
class="w-full pl-10 pr-4 py-3 bg-white text-sm text-slate-800 placeholder-slate-400 border border-slate-200/80 rounded-lg focus:outline-none focus:border-brand-dark focus:ring-1 focus:ring-brand-dark transition-all duration-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TABELLA CATALOGO -->
|
||||||
|
<div class="bg-white rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse table-fixed min-w-[1000px]">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-slate-100/80 bg-slate-50/50 text-slate-400 text-[11px] font-semibold uppercase tracking-wider">
|
||||||
|
<th class="py-3 px-5 w-1/3">
|
||||||
|
<span class="inline-flex items-center gap-1 cursor-pointer hover:text-slate-700">
|
||||||
|
Nome
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M8 9l4-4 4 4m0 6l-4 4-4-4"/></svg>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 px-4 w-[110px]">
|
||||||
|
<span class="inline-flex items-center gap-1 cursor-pointer hover:text-slate-700">
|
||||||
|
Prezzo
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M8 9l4-4 4 4m0 6l-4 4-4-4"/></svg>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th class="py-3 px-4 w-[160px]">Offerta</th>
|
||||||
|
<th class="py-3 px-4 w-[240px]">Fase</th>
|
||||||
|
<th class="py-3 px-4 w-1/5">Descrizione</th>
|
||||||
|
<th class="py-3 px-4 w-[100px] text-center">Pacchetto</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100/60 text-[13px] text-slate-700">
|
||||||
|
|
||||||
|
<!-- Riga 1 -->
|
||||||
|
<tr class="hover:bg-slate-50/30 transition-all duration-150">
|
||||||
|
<td class="py-3 px-5 font-medium text-slate-900 truncate">Landing Page (Metodo o Differenziante)</td>
|
||||||
|
<td class="py-3 px-4 font-mono text-xs text-slate-600">€1.000,00</td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded text-[10px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-100">
|
||||||
|
Signature Offer
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-medium bg-amber-50 text-amber-700 border border-amber-100">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span>
|
||||||
|
Fase 3 ➔ Esecuzione
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-slate-400 text-xs truncate">—</td>
|
||||||
|
<td class="py-3 px-4 text-center">
|
||||||
|
<button class="w-6 h-6 rounded-full border border-slate-200 text-slate-400 hover:text-brand-dark hover:border-brand-dark hover:bg-emerald-50/30 flex items-center justify-center mx-auto transition-all">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Riga 2 -->
|
||||||
|
<tr class="hover:bg-slate-50/30 transition-all duration-150">
|
||||||
|
<td class="py-3 px-5 font-medium text-slate-900 truncate">Analisi Competitor</td>
|
||||||
|
<td class="py-3 px-4 font-mono text-xs text-slate-600">€400,00</td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<div class="flex flex-col gap-1 items-start">
|
||||||
|
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded text-[10px] font-semibold bg-purple-50 text-purple-600 border border-purple-100">
|
||||||
|
Entry Offer
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded text-[10px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-100">
|
||||||
|
Signature Offer
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-medium bg-blue-50 text-blue-700 border border-blue-100">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-blue-400"></span>
|
||||||
|
Fase 2 ➔ Analisi / Strat.
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-slate-400 text-xs truncate">—</td>
|
||||||
|
<td class="py-3 px-4 text-center">
|
||||||
|
<button class="w-6 h-6 rounded-full border border-slate-200 text-slate-400 hover:text-brand-dark hover:border-brand-dark hover:bg-emerald-50/30 flex items-center justify-center mx-auto transition-all">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Riga 3 -->
|
||||||
|
<tr class="hover:bg-slate-50/30 transition-all duration-150">
|
||||||
|
<td class="py-3 px-5 font-medium text-slate-900 truncate">Art direction su direzione da prendere</td>
|
||||||
|
<td class="py-3 px-4 font-mono text-xs text-slate-600">€2.000,00</td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded text-[10px] font-semibold bg-blue-50 text-blue-600 border border-blue-100">
|
||||||
|
Retainer Offer
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-slate-400 text-xs">—</td>
|
||||||
|
<td class="py-3 px-4 text-slate-400 text-xs truncate">—</td>
|
||||||
|
<td class="py-3 px-4 text-center">
|
||||||
|
<button class="w-6 h-6 rounded-full border border-slate-200 text-slate-400 hover:text-brand-dark hover:border-brand-dark hover:bg-emerald-50/30 flex items-center justify-center mx-auto transition-all">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Riga 4 -->
|
||||||
|
<tr class="hover:bg-slate-50/30 transition-all duration-150">
|
||||||
|
<td class="py-3 px-5 font-medium text-slate-900 truncate">Audit iniziale (UX/UI, struttura, conversione)</td>
|
||||||
|
<td class="py-3 px-4 font-mono text-xs text-slate-600">€500,00</td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<div class="flex flex-col gap-1 items-start">
|
||||||
|
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded text-[10px] font-semibold bg-purple-50 text-purple-600 border border-purple-100">
|
||||||
|
Entry Offer
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded text-[10px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-100">
|
||||||
|
Signature Offer
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-medium bg-orange-50 text-orange-700 border border-orange-100">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-orange-400"></span>
|
||||||
|
Fase 1 ➔ Onboarding
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-slate-400 text-xs truncate">—</td>
|
||||||
|
<td class="py-3 px-4 text-center">
|
||||||
|
<button class="w-6 h-6 rounded-full border border-slate-200 text-slate-400 hover:text-brand-dark hover:border-brand-dark hover:bg-emerald-50/30 flex items-center justify-center mx-auto transition-all">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Riga 5 -->
|
||||||
|
<tr class="hover:bg-slate-50/30 transition-all duration-150">
|
||||||
|
<td class="py-3 px-5 font-medium text-slate-900 truncate">Go-live / messa online & accessi</td>
|
||||||
|
<td class="py-3 px-4 font-mono text-xs text-slate-600">€300,00</td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded text-[10px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-100">
|
||||||
|
Signature Offer
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-medium bg-teal-50 text-teal-700 border border-teal-100">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-teal-400"></span>
|
||||||
|
Fase 5 ➔ Consegna
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-slate-400 text-xs truncate">—</td>
|
||||||
|
<td class="py-3 px-4 text-center">
|
||||||
|
<button class="w-6 h-6 rounded-full border border-slate-200 text-slate-400 hover:text-brand-dark hover:border-brand-dark hover:bg-emerald-50/30 flex items-center justify-center mx-auto transition-all">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- FORM INLINE PER NUOVA RIGA ("Aggiungi servizio") -->
|
||||||
|
<tr class="bg-slate-50/40 border-t-2 border-slate-100">
|
||||||
|
<td class="py-4 px-5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="+ Aggiungi servizio..."
|
||||||
|
class="w-full bg-white border border-slate-200 rounded px-3 py-1.5 text-xs text-slate-800 placeholder-slate-400 focus:outline-none focus:border-brand-dark focus:ring-1 focus:ring-brand-dark transition-all"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="0,00"
|
||||||
|
class="w-full bg-white border border-slate-200 rounded px-3 py-1.5 text-xs font-mono text-slate-800 placeholder-slate-400 focus:outline-none focus:border-brand-dark focus:ring-1 focus:ring-brand-dark transition-all"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-4 text-slate-300 text-xs select-none">Seleziona...</td>
|
||||||
|
<td class="py-4 px-4">
|
||||||
|
<select class="w-full bg-white border border-slate-200 rounded px-2 py-1.5 text-xs text-slate-600 focus:outline-none focus:border-brand-dark focus:ring-1 focus:ring-brand-dark transition-all">
|
||||||
|
<option value="">Fase...</option>
|
||||||
|
<option value="1">Fase 1</option>
|
||||||
|
<option value="2">Fase 2</option>
|
||||||
|
<option value="3">Fase 3</option>
|
||||||
|
<option value="4">Fase 4</option>
|
||||||
|
<option value="5">Fase 5</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Descrizione..."
|
||||||
|
class="w-full bg-white border border-slate-200 rounded px-3 py-1.5 text-xs text-slate-800 placeholder-slate-400 focus:outline-none focus:border-brand-dark focus:ring-1 focus:ring-brand-dark transition-all"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-4 text-center">
|
||||||
|
<button class="bg-brand-dark hover:bg-brand-darkHover text-white text-xs font-medium px-4 py-1.5 rounded transition-all">
|
||||||
|
Invia
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FOOTER TABELLA -->
|
||||||
|
<div class="border-t border-slate-100 px-6 py-4 flex items-center justify-between text-xs text-slate-400">
|
||||||
|
<span>Visualizzazione di 5 servizi principali</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SCRIPT DI LOGICA (Gestione Sidebar) -->
|
||||||
|
<script>
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const toggleButton = document.getElementById('sidebar-toggle');
|
||||||
|
const logoText = document.getElementById('sidebar-logo-text');
|
||||||
|
const sidebarTexts = document.querySelectorAll('.sidebar-text');
|
||||||
|
|
||||||
|
toggleButton.addEventListener('click', () => {
|
||||||
|
const isCollapsed = sidebar.classList.contains('w-20');
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
sidebar.classList.remove('w-20');
|
||||||
|
sidebar.classList.add('w-64');
|
||||||
|
setTimeout(() => {
|
||||||
|
logoText.classList.remove('opacity-0');
|
||||||
|
sidebarTexts.forEach(text => text.classList.remove('opacity-0'));
|
||||||
|
}, 150);
|
||||||
|
} else {
|
||||||
|
logoText.classList.add('opacity-0');
|
||||||
|
sidebarTexts.forEach(text => text.classList.add('opacity-0'));
|
||||||
|
sidebar.classList.remove('w-64');
|
||||||
|
sidebar.classList.add('w-20');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Clienti — Luxury Admin CRM</title>
|
||||||
|
<!-- Google Fonts: Plus Jakarta Sans -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<!-- Tailwind CSS CDN -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Plus Jakarta Sans', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: {
|
||||||
|
dark: '#1A463C', /* Verde scuro richiesto */
|
||||||
|
darkHover: '#13342D', /* Variante scura per hover */
|
||||||
|
active: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
bg: '#F8F9FA',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.sidebar-transition {
|
||||||
|
transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1), transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-brand-bg font-sans text-slate-800 antialiased min-h-screen flex overflow-x-hidden">
|
||||||
|
|
||||||
|
<!-- SIDEBAR -->
|
||||||
|
<aside id="sidebar" class="w-64 bg-brand-dark text-white flex flex-col justify-between p-6 border-r border-emerald-950/20 shrink-0 sidebar-transition relative z-10">
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<!-- Logo e Intestazione -->
|
||||||
|
<div class="mb-10 px-2 flex items-center justify-between">
|
||||||
|
<span id="sidebar-logo-text" class="text-lg font-bold tracking-wider text-emerald-50 whitespace-nowrap transition-opacity duration-300">iamcavalli</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigazione Principale -->
|
||||||
|
<nav class="space-y-1">
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2v-4zM14 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2v-4z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Lead</span>
|
||||||
|
</a>
|
||||||
|
<!-- Stato Attivo su Clienti -->
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-white bg-brand-active font-medium transition-all duration-200 shadow-sm">
|
||||||
|
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Clienti</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Progetti</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Preventivi</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M7 7h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Offerte</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Area Inferiore Sidebar -->
|
||||||
|
<div class="border-t border-white/10 pt-4 space-y-3 overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between px-3 py-1 text-xs text-emerald-200/50 whitespace-nowrap">
|
||||||
|
<span class="sidebar-text transition-opacity duration-300">Tema</span>
|
||||||
|
<button class="p-1 rounded-full bg-white/5 hover:bg-white/10 text-emerald-200 hover:text-white transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707M14 12a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-red-300/80 hover:text-red-200 hover:bg-red-950/20 transition-all duration-200 whitespace-nowrap">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
|
||||||
|
<span class="sidebar-text transition-opacity duration-300">Esci</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- SEZIONE PRINCIPALE -->
|
||||||
|
<div class="flex-1 flex flex-col min-w-0">
|
||||||
|
|
||||||
|
<!-- BARRA DI INTESTAZIONE SUPERIORE -->
|
||||||
|
<header class="h-16 border-b border-slate-100 bg-white px-8 flex items-center justify-between shrink-0">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<!-- Pulsante Apri/Chiudi Sidebar -->
|
||||||
|
<button id="sidebar-toggle" class="p-2 -ml-2 rounded-lg text-slate-500 hover:bg-slate-50 hover:text-slate-800 transition-colors focus:outline-none" title="Espandi/Comprimi Sidebar">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 6h16M4 12h12M4 18h16"/></svg>
|
||||||
|
</button>
|
||||||
|
<span class="text-xs text-slate-400 font-medium">Area di Lavoro</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="text-xs font-semibold text-slate-700 bg-slate-100 px-2.5 py-1 rounded-full">Demo Account</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- AREA DEL CONTENUTO DINAMICO -->
|
||||||
|
<main class="flex-1 p-8 lg:p-10 max-w-[1400px] w-full mx-auto flex flex-col gap-8 overflow-y-auto">
|
||||||
|
|
||||||
|
<!-- INTESTAZIONE SEZIONE -->
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between sm:items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-semibold text-slate-900 tracking-tight">Clienti</h1>
|
||||||
|
<p class="text-xs text-slate-400 mt-1">Monitora i rapporti finanziari e i dettagli dei clienti attivi</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pulsanti di azione -->
|
||||||
|
<div class="flex items-center gap-4 self-start sm:self-auto">
|
||||||
|
<button class="text-xs font-medium text-slate-500 hover:text-slate-900 transition-colors">
|
||||||
|
Mostra archiviati
|
||||||
|
</button>
|
||||||
|
<button class="bg-brand-dark hover:bg-brand-darkHover text-white text-xs font-medium tracking-wide px-5 py-3 rounded-lg flex items-center gap-2 transition-all duration-200 shadow-sm">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||||
|
Nuovo cliente
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TABELLA CLIENTI -->
|
||||||
|
<div class="bg-white rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-slate-100/80 bg-slate-50/50 text-slate-400 text-[11px] font-semibold uppercase tracking-wider">
|
||||||
|
<th class="py-4 px-6 w-1/4">Cliente</th>
|
||||||
|
<th class="py-4 px-6 text-right">LTV</th>
|
||||||
|
<th class="py-4 px-6 text-right">Importo Incassato</th>
|
||||||
|
<th class="py-4 px-6 text-right">Importo da Saldare</th>
|
||||||
|
<th class="py-4 px-6 text-right">Link Profilo</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100/60 text-sm">
|
||||||
|
|
||||||
|
<!-- Riga 1: George Vlad -->
|
||||||
|
<tr class="hover:bg-slate-50/30 transition-all duration-150">
|
||||||
|
<td class="py-4 px-6">
|
||||||
|
<div class="font-semibold text-slate-950">George Vlad</div>
|
||||||
|
<div class="text-[11px] text-slate-400 mt-0.5">Protocollo Estetico | Incarichi Online</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-right font-medium text-slate-900">€7.000,00</td>
|
||||||
|
<td class="py-4 px-6 text-right">
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded text-xs font-semibold bg-emerald-50 text-emerald-700">
|
||||||
|
€2.800,00
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-right font-medium text-slate-600">€4.200,00</td>
|
||||||
|
<td class="py-4 px-6 text-right">
|
||||||
|
<div class="inline-flex items-center gap-2 justify-end">
|
||||||
|
<span class="text-xs font-mono text-slate-400 bg-slate-50 px-2 py-1 rounded border border-slate-100">/client/george-vlad-...</span>
|
||||||
|
<button class="p-1 hover:bg-slate-100 rounded text-slate-400 hover:text-slate-700 transition-colors" title="Copia Link">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Riga 2: Gian Luca Caruso -->
|
||||||
|
<tr class="hover:bg-slate-50/30 transition-all duration-150">
|
||||||
|
<td class="py-4 px-6">
|
||||||
|
<div class="font-semibold text-slate-950">Gian Luca Caruso</div>
|
||||||
|
<div class="text-[11px] text-slate-400 mt-0.5">Caruso Speaker</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-right font-medium text-slate-900">€5.000,00</td>
|
||||||
|
<td class="py-4 px-6 text-right">
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded text-xs font-semibold bg-emerald-50 text-emerald-700">
|
||||||
|
€2.500,00
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-right font-medium text-slate-600">€2.500,00</td>
|
||||||
|
<td class="py-4 px-6 text-right">
|
||||||
|
<div class="inline-flex items-center gap-2 justify-end">
|
||||||
|
<span class="text-xs font-mono text-slate-400 bg-slate-50 px-2 py-1 rounded border border-slate-100">/client/gian-luca-ca...</span>
|
||||||
|
<button class="p-1 hover:bg-slate-100 rounded text-slate-400 hover:text-slate-700 transition-colors" title="Copia Link">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Riga 3: Gianfranco Barban -->
|
||||||
|
<tr class="hover:bg-slate-50/30 transition-all duration-150">
|
||||||
|
<td class="py-4 px-6">
|
||||||
|
<div class="font-semibold text-slate-950">Gianfranco Barban</div>
|
||||||
|
<div class="text-[11px] text-slate-400 mt-0.5">Teckell</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-right font-medium text-slate-900">€200,00</td>
|
||||||
|
<td class="py-4 px-6 text-right text-slate-300 font-medium">—</td>
|
||||||
|
<td class="py-4 px-6 text-right text-slate-300 font-medium">—</td>
|
||||||
|
<td class="py-4 px-6 text-right">
|
||||||
|
<div class="inline-flex items-center gap-2 justify-end">
|
||||||
|
<span class="text-xs font-mono text-slate-400 bg-slate-50 px-2 py-1 rounded border border-slate-100">/client/gianfranco-b...</span>
|
||||||
|
<button class="p-1 hover:bg-slate-100 rounded text-slate-400 hover:text-slate-700 transition-colors" title="Copia Link">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Riga 4: Mario Rossi -->
|
||||||
|
<tr class="hover:bg-slate-50/30 transition-all duration-150">
|
||||||
|
<td class="py-4 px-6">
|
||||||
|
<div class="font-semibold text-slate-950">Mario Rossi</div>
|
||||||
|
<div class="text-[11px] text-slate-400 mt-0.5">Rossi Inc</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-right font-medium text-slate-900">€7.000,00</td>
|
||||||
|
<td class="py-4 px-6 text-right text-slate-300 font-medium">—</td>
|
||||||
|
<td class="py-4 px-6 text-right font-medium text-slate-600">€7.000,00</td>
|
||||||
|
<td class="py-4 px-6 text-right">
|
||||||
|
<div class="inline-flex items-center gap-2 justify-end">
|
||||||
|
<span class="text-xs font-mono text-slate-400 bg-slate-50 px-2 py-1 rounded border border-slate-100">/client/mario-rossi-...</span>
|
||||||
|
<button class="p-1 hover:bg-slate-100 rounded text-slate-400 hover:text-slate-700 transition-colors" title="Copia Link">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Tabella -->
|
||||||
|
<div class="border-t border-slate-100 px-6 py-4 flex items-center justify-between text-xs text-slate-400">
|
||||||
|
<span>Mostrando 4 di 4 clienti attivi</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SCRIPT DI LOGICA (Gestione Sidebar) -->
|
||||||
|
<script>
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const toggleButton = document.getElementById('sidebar-toggle');
|
||||||
|
const logoText = document.getElementById('sidebar-logo-text');
|
||||||
|
const sidebarTexts = document.querySelectorAll('.sidebar-text');
|
||||||
|
|
||||||
|
toggleButton.addEventListener('click', () => {
|
||||||
|
const isCollapsed = sidebar.classList.contains('w-20');
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
sidebar.classList.remove('w-20');
|
||||||
|
sidebar.classList.add('w-64');
|
||||||
|
setTimeout(() => {
|
||||||
|
logoText.classList.remove('opacity-0');
|
||||||
|
sidebarTexts.forEach(text => text.classList.remove('opacity-0'));
|
||||||
|
}, 150);
|
||||||
|
} else {
|
||||||
|
logoText.classList.add('opacity-0');
|
||||||
|
sidebarTexts.forEach(text => text.classList.add('opacity-0'));
|
||||||
|
sidebar.classList.remove('w-64');
|
||||||
|
sidebar.classList.add('w-20');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,484 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Rossi Inc — Client Portal</title>
|
||||||
|
<!-- Google Fonts: Plus Jakarta Sans -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<!-- Tailwind CSS CDN -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<!-- Canvas Confetti per micro-interazioni celebrative -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Plus Jakarta Sans', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: {
|
||||||
|
dark: '#1A463C', /* Verde lusso */
|
||||||
|
darkHover: '#13342D',
|
||||||
|
lightBg: '#F8F9FA',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.smooth-transition {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
/* Stili per il Dragging del Kanban */
|
||||||
|
.dragging {
|
||||||
|
opacity: 0.4;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
.drag-over-zone {
|
||||||
|
background-color: rgba(26, 70, 60, 0.03);
|
||||||
|
border-color: rgba(26, 70, 60, 0.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-brand-lightBg font-sans text-slate-800 antialiased min-h-screen pb-16">
|
||||||
|
|
||||||
|
<!-- HEADER PORTALE -->
|
||||||
|
<header class="bg-white border-b border-slate-100 px-8 py-5 sticky top-0 z-50 shadow-sm flex flex-col md:flex-row justify-between items-center gap-4">
|
||||||
|
<div class="flex items-center gap-3 w-full md:w-auto">
|
||||||
|
<span class="text-xs font-bold uppercase tracking-widest text-slate-400">iamcavalli</span>
|
||||||
|
<span class="text-slate-300">|</span>
|
||||||
|
<span class="text-xs font-medium text-slate-500">Client Portal</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rossi Inc al Centro -->
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-xl font-bold text-slate-900 tracking-tight">Rossi Inc</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden md:flex items-center gap-2 text-[11px] text-emerald-700 bg-emerald-50 px-3 py-1 rounded-full border border-emerald-100">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||||
|
Area Riservata Protetta
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- STEP PROGRESS TIMELINE UNIFICATA -->
|
||||||
|
<div class="bg-white border-b border-slate-100 py-8 px-6">
|
||||||
|
<div class="max-w-[1200px] mx-auto relative">
|
||||||
|
<!-- Linea di connessione di fondo -->
|
||||||
|
<div class="absolute top-[15px] left-[5%] right-[5%] h-1 bg-slate-100 rounded-full z-0">
|
||||||
|
<!-- Riempimento dinamico fisso basato sul progresso reale del progetto -->
|
||||||
|
<div class="bg-brand-dark h-full rounded-full smooth-transition" style="width: 35%;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contenitore Nodi Milestone -->
|
||||||
|
<div class="relative z-10 grid grid-cols-5 gap-4">
|
||||||
|
|
||||||
|
<!-- Step 1 -->
|
||||||
|
<div class="flex flex-col items-center text-center">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-brand-dark text-white flex items-center justify-center text-xs font-bold shadow-md border-4 border-white smooth-transition">
|
||||||
|
✓
|
||||||
|
</div>
|
||||||
|
<span class="text-[11px] font-bold text-slate-900 mt-2">Step 1</span>
|
||||||
|
<span class="text-[10px] text-slate-400 font-medium">Onboarding</span>
|
||||||
|
<span class="text-[9px] font-bold text-emerald-600 mt-1 uppercase">Completed</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2 -->
|
||||||
|
<div class="flex flex-col items-center text-center">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-white text-brand-dark border-4 border-brand-dark flex items-center justify-center text-xs font-bold shadow-md smooth-transition">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<span class="text-[11px] font-bold text-slate-900 mt-2">Step 2</span>
|
||||||
|
<span class="text-[10px] text-slate-500 font-medium">Analisi</span>
|
||||||
|
<span class="text-[9px] font-bold text-amber-600 mt-1 uppercase">In Progress</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3 -->
|
||||||
|
<div class="flex flex-col items-center text-center">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-slate-100 text-slate-400 border-4 border-white flex items-center justify-center text-xs font-semibold smooth-transition">
|
||||||
|
3
|
||||||
|
</div>
|
||||||
|
<span class="text-[11px] font-bold text-slate-500 mt-2">Step 3</span>
|
||||||
|
<span class="text-[10px] text-slate-400 font-medium">Esecuzione</span>
|
||||||
|
<span class="text-[9px] font-bold text-slate-400 mt-1 uppercase">Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 4 -->
|
||||||
|
<div class="flex flex-col items-center text-center">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-slate-100 text-slate-400 border-4 border-white flex items-center justify-center text-xs font-semibold smooth-transition">
|
||||||
|
4
|
||||||
|
</div>
|
||||||
|
<span class="text-[11px] font-bold text-slate-500 mt-2">Step 4</span>
|
||||||
|
<span class="text-[10px] text-slate-400 font-medium">Raffinamento</span>
|
||||||
|
<span class="text-[9px] font-bold text-slate-400 mt-1 uppercase">Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 5 -->
|
||||||
|
<div class="flex flex-col items-center text-center">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-slate-100 text-slate-400 border-4 border-white flex items-center justify-center text-xs font-semibold smooth-transition">
|
||||||
|
5
|
||||||
|
</div>
|
||||||
|
<span class="text-[11px] font-bold text-slate-500 mt-2">Step 5</span>
|
||||||
|
<span class="text-[10px] text-slate-400 font-medium">Consegna</span>
|
||||||
|
<span class="text-[9px] font-bold text-slate-400 mt-1 uppercase">Pending</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SEZIONE PRINCIPALE CONTENUTO -->
|
||||||
|
<main class="max-w-[1400px] mx-auto px-6 mt-10 grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||||
|
|
||||||
|
<!-- COLONNA DI SINISTRA (Informazioni statiche di riepilogo) -->
|
||||||
|
<div class="lg:col-span-1 flex flex-col gap-6">
|
||||||
|
|
||||||
|
<!-- Offerte Attive -->
|
||||||
|
<div class="bg-white rounded-xl border border-slate-100 p-5 shadow-sm">
|
||||||
|
<h3 class="text-xs font-bold uppercase tracking-wider text-slate-400 mb-4">Offerte Attive</h3>
|
||||||
|
<div class="border-l-2 border-brand-dark pl-3">
|
||||||
|
<h4 class="text-sm font-bold text-slate-900">Web Domination</h4>
|
||||||
|
<div class="flex justify-between items-center text-xs mt-2 text-slate-500">
|
||||||
|
<span>Prezzo finale</span>
|
||||||
|
<span class="font-bold text-brand-dark">€7.000,00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 pt-4 border-t border-slate-100">
|
||||||
|
<button onclick="toggleAccordion('compreso-content')" class="flex justify-between items-center w-full text-xs font-semibold text-slate-600 hover:text-slate-950">
|
||||||
|
<span>Cosa è compreso</span>
|
||||||
|
<svg id="compreso-arrow" class="w-4 h-4 transform transition-transform" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19.5 8.25l-7.5 7.5-7.5-7.5"/></svg>
|
||||||
|
</button>
|
||||||
|
<div id="compreso-content" class="hidden mt-2 text-[11px] text-slate-500 space-y-1">
|
||||||
|
<p>• Strategia e architettura Web</p>
|
||||||
|
<p>• Sviluppo custom Webflow</p>
|
||||||
|
<p>• Ottimizzazione SEO specialistica</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagamenti -->
|
||||||
|
<div class="bg-white rounded-xl border border-slate-100 p-5 shadow-sm">
|
||||||
|
<h3 class="text-xs font-bold uppercase tracking-wider text-slate-400 mb-4">Pagamenti</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between items-center p-3 rounded-lg border border-slate-100 text-xs">
|
||||||
|
<span class="font-semibold text-slate-700">Saldo 50%</span>
|
||||||
|
<span class="px-2 py-1 rounded text-[10px] font-bold bg-amber-50 text-amber-700 border border-amber-100 uppercase">Da Saldare</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center p-3 rounded-lg border border-slate-100 text-xs">
|
||||||
|
<span class="font-semibold text-slate-700">Acconto 50%</span>
|
||||||
|
<span class="px-2 py-1 rounded text-[10px] font-bold bg-emerald-50 text-emerald-700 border border-emerald-100 uppercase">Saldato</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Documenti -->
|
||||||
|
<div class="bg-white rounded-xl border border-slate-100 p-5 shadow-sm">
|
||||||
|
<h3 class="text-xs font-bold uppercase tracking-wider text-slate-400 mb-4">Documenti & File</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<a href="#" class="flex items-center justify-between p-3 rounded-lg border border-slate-100 text-xs hover:border-slate-300 transition-colors">
|
||||||
|
<span class="text-slate-700 font-medium">Brief Progetto</span>
|
||||||
|
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- COLONNA DI DESTRA (Area di lavoro commutabile) -->
|
||||||
|
<div class="lg:col-span-3 flex flex-col gap-6">
|
||||||
|
|
||||||
|
<!-- CONTROLLI DELLA VISTA -->
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h2 id="view-title" class="text-lg font-bold text-slate-900 tracking-tight">Fasi del Progetto</h2>
|
||||||
|
<div class="bg-slate-200/50 p-1 rounded-lg flex gap-1">
|
||||||
|
<button id="btn-timeline" class="px-5 py-1.5 bg-white rounded-md text-[11px] font-semibold text-slate-800 shadow-sm transition-all duration-200">
|
||||||
|
Timeline
|
||||||
|
</button>
|
||||||
|
<button id="btn-kanban" class="px-5 py-1.5 rounded-md text-[11px] font-semibold text-slate-500 hover:text-slate-800 transition-all duration-200">
|
||||||
|
Kanban
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ================= VISTA 1: TIMELINE (Read-only da backend) ================= -->
|
||||||
|
<div id="view-timeline" class="space-y-6">
|
||||||
|
|
||||||
|
<!-- FASE 1: COMPLETATA -->
|
||||||
|
<div class="bg-white rounded-xl border border-slate-100 p-6 shadow-sm">
|
||||||
|
<div class="flex justify-between items-center mb-3">
|
||||||
|
<h3 class="text-sm font-bold text-slate-900">Fase 1 ➔ Onboarding / Setup</h3>
|
||||||
|
<span class="px-2.5 py-0.5 rounded text-[10px] font-bold bg-emerald-50 text-emerald-700 border border-emerald-100 uppercase">Completata</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden mb-4">
|
||||||
|
<div class="bg-emerald-600 h-full" style="width: 100%;"></div>
|
||||||
|
</div>
|
||||||
|
<ul class="space-y-2.5 text-xs text-slate-600 pt-2 border-t border-slate-50">
|
||||||
|
<li class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 rounded-full bg-emerald-50 text-emerald-600 flex items-center justify-center text-[10px] font-bold">✓</span>
|
||||||
|
<span class="line-through text-slate-400">Audit iniziale (UX/UI, struttura, conversione)</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 rounded-full bg-emerald-50 text-emerald-600 flex items-center justify-center text-[10px] font-bold">✓</span>
|
||||||
|
<span class="line-through text-slate-400">Raccolta e Mappatura Materiali</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 rounded-full bg-emerald-50 text-emerald-600 flex items-center justify-center text-[10px] font-bold">✓</span>
|
||||||
|
<span class="line-through text-slate-400">Workshop 1° fase</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FASE 2: IN CORSO -->
|
||||||
|
<div class="bg-white rounded-xl border border-slate-100 p-6 shadow-sm">
|
||||||
|
<div class="flex justify-between items-center mb-3">
|
||||||
|
<h3 class="text-sm font-bold text-slate-900">Fase 2 ➔ Analisi / Strategia</h3>
|
||||||
|
<span class="px-2.5 py-0.5 rounded text-[10px] font-bold bg-amber-50 text-amber-700 border border-amber-100 uppercase">In corso</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden mb-4">
|
||||||
|
<div class="bg-amber-500 h-full" style="width: 30%;"></div>
|
||||||
|
</div>
|
||||||
|
<ul class="space-y-2.5 text-xs text-slate-600 pt-2 border-t border-slate-50">
|
||||||
|
<li class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 rounded-full border-2 border-slate-200 bg-white flex items-center justify-center"></span>
|
||||||
|
<span>Analisi Competitor</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 rounded-full border-2 border-slate-200 bg-white flex items-center justify-center"></span>
|
||||||
|
<span>Architettura informativa (Sitemap)</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 rounded-full border-2 border-slate-200 bg-white flex items-center justify-center"></span>
|
||||||
|
<span>Brand Identity - Visual Identity</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FASE 3: DA INIZIARE -->
|
||||||
|
<div class="bg-white rounded-xl border border-slate-100 p-6 shadow-sm">
|
||||||
|
<div class="flex justify-between items-center mb-3">
|
||||||
|
<h3 class="text-sm font-bold text-slate-900">Fase 3 ➔ Esecuzione / Core</h3>
|
||||||
|
<span class="px-2.5 py-0.5 rounded text-[10px] font-bold bg-slate-100 text-slate-500 border border-slate-200 uppercase">Da iniziare</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden mb-4">
|
||||||
|
<div class="bg-slate-300 h-full" style="width: 0%;"></div>
|
||||||
|
</div>
|
||||||
|
<ul class="space-y-2.5 text-xs text-slate-600 pt-2 border-t border-slate-50">
|
||||||
|
<li class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 rounded-full border-2 border-slate-200 bg-white flex items-center justify-center"></span>
|
||||||
|
<span>Homepage Figma & Webflow development</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 rounded-full border-2 border-slate-200 bg-white flex items-center justify-center"></span>
|
||||||
|
<span>Configurazione CMS Blog e sezioni dinamiche</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FASE 4: DA INIZIARE -->
|
||||||
|
<div class="bg-white rounded-xl border border-slate-100 p-6 shadow-sm">
|
||||||
|
<div class="flex justify-between items-center mb-3">
|
||||||
|
<h3 class="text-sm font-bold text-slate-900">Fase 4 ➔ Raffinamento / Extra</h3>
|
||||||
|
<span class="px-2.5 py-0.5 rounded text-[10px] font-bold bg-slate-100 text-slate-500 border border-slate-200 uppercase">Da iniziare</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden mb-4">
|
||||||
|
<div class="bg-slate-300 h-full" style="width: 0%;"></div>
|
||||||
|
</div>
|
||||||
|
<ul class="space-y-2.5 text-xs text-slate-600 pt-2 border-t border-slate-50">
|
||||||
|
<li class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 rounded-full border-2 border-slate-200 bg-white flex items-center justify-center"></span>
|
||||||
|
<span>Ottimizzazione Web Core Vitals</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FASE 5: DA INIZIARE -->
|
||||||
|
<div class="bg-white rounded-xl border border-slate-100 p-6 shadow-sm">
|
||||||
|
<div class="flex justify-between items-center mb-3">
|
||||||
|
<h3 class="text-sm font-bold text-slate-900">Fase 5 ➔ Onboarding / Consegna</h3>
|
||||||
|
<span class="px-2.5 py-0.5 rounded text-[10px] font-bold bg-slate-100 text-slate-500 border border-slate-200 uppercase">Da iniziare</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden mb-4">
|
||||||
|
<div class="bg-slate-300 h-full" style="width: 0%;"></div>
|
||||||
|
</div>
|
||||||
|
<ul class="space-y-2.5 text-xs text-slate-600 pt-2 border-t border-slate-50">
|
||||||
|
<li class="flex items-center gap-3">
|
||||||
|
<span class="w-5 h-5 rounded-full border-2 border-slate-200 bg-white flex items-center justify-center"></span>
|
||||||
|
<span>Rilascio e puntamento DNS live</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ================= VISTA 2: KANBAN ATTIVO & TRASCINABILE ================= -->
|
||||||
|
<div id="view-kanban" class="hidden grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
|
||||||
|
<!-- COLONNA: DA FARE -->
|
||||||
|
<div class="kanban-column bg-slate-50/80 border border-slate-100 rounded-xl p-4 flex flex-col gap-4 min-h-[500px]" data-status="todo">
|
||||||
|
<div class="flex justify-between items-center border-b border-slate-200/60 pb-2">
|
||||||
|
<span class="text-xs font-bold text-slate-500 uppercase tracking-wider">Da Fare</span>
|
||||||
|
<span class="count-pill bg-slate-200 text-slate-600 text-[10px] font-bold px-2 py-0.5 rounded-full">3</span>
|
||||||
|
</div>
|
||||||
|
<div class="cards-dropzone flex-1 flex flex-col gap-3">
|
||||||
|
<!-- Card 1 -->
|
||||||
|
<div class="kanban-card bg-white p-4 rounded-lg border border-slate-200/60 shadow-sm cursor-grab active:cursor-grabbing hover:shadow transition-all" draggable="true" id="task-1">
|
||||||
|
<span class="text-[9px] font-semibold text-slate-400 block mb-1">FASE 2</span>
|
||||||
|
<p class="text-xs font-medium text-slate-800">Analisi Competitor</p>
|
||||||
|
</div>
|
||||||
|
<!-- Card 2 -->
|
||||||
|
<div class="kanban-card bg-white p-4 rounded-lg border border-slate-200/60 shadow-sm cursor-grab active:cursor-grabbing hover:shadow transition-all" draggable="true" id="task-2">
|
||||||
|
<span class="text-[9px] font-semibold text-slate-400 block mb-1">FASE 2</span>
|
||||||
|
<p class="text-xs font-medium text-slate-800">Architettura Informativa (Sitemap)</p>
|
||||||
|
</div>
|
||||||
|
<!-- Card 3 -->
|
||||||
|
<div class="kanban-card bg-white p-4 rounded-lg border border-slate-200/60 shadow-sm cursor-grab active:cursor-grabbing hover:shadow transition-all" draggable="true" id="task-3">
|
||||||
|
<span class="text-[9px] font-semibold text-slate-400 block mb-1">FASE 2</span>
|
||||||
|
<p class="text-xs font-medium text-slate-800">Brand Identity - Visual Identity</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- COLONNA: IN CORSO -->
|
||||||
|
<div class="kanban-column bg-slate-50/80 border border-slate-100 rounded-xl p-4 flex flex-col gap-4 min-h-[500px]" data-status="inprogress">
|
||||||
|
<div class="flex justify-between items-center border-b border-slate-200/60 pb-2">
|
||||||
|
<span class="text-xs font-bold text-slate-500 uppercase tracking-wider">In Corso</span>
|
||||||
|
<span class="count-pill bg-slate-200 text-slate-600 text-[10px] font-bold px-2 py-0.5 rounded-full">1</span>
|
||||||
|
</div>
|
||||||
|
<div class="cards-dropzone flex-1 flex flex-col gap-3">
|
||||||
|
<!-- Card 4 -->
|
||||||
|
<div class="kanban-card bg-white p-4 rounded-lg border border-slate-200/60 shadow-sm cursor-grab active:cursor-grabbing hover:shadow transition-all" draggable="true" id="task-4">
|
||||||
|
<span class="text-[9px] font-semibold text-slate-400 block mb-1">FASE 2</span>
|
||||||
|
<p class="text-xs font-medium text-slate-800">Direzione Creativa & Moodboard</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- COLONNA: FATTO -->
|
||||||
|
<div class="kanban-column bg-slate-50/80 border border-slate-100 rounded-xl p-4 flex flex-col gap-4 min-h-[500px]" data-status="done">
|
||||||
|
<div class="flex justify-between items-center border-b border-slate-200/60 pb-2">
|
||||||
|
<span class="text-xs font-bold text-slate-500 uppercase tracking-wider">Fatto</span>
|
||||||
|
<span class="count-pill bg-slate-200 text-slate-600 text-[10px] font-bold px-2 py-0.5 rounded-full">2</span>
|
||||||
|
</div>
|
||||||
|
<div class="cards-dropzone flex-1 flex flex-col gap-3">
|
||||||
|
<!-- Card 5 -->
|
||||||
|
<div class="kanban-card bg-white p-4 rounded-lg border border-slate-200/60 shadow-sm cursor-grab active:cursor-grabbing hover:shadow transition-all" draggable="true" id="task-5">
|
||||||
|
<span class="text-[9px] font-semibold text-slate-400 block mb-1">FASE 1</span>
|
||||||
|
<p class="text-xs font-medium text-slate-800">Kickoff strategico iniziale</p>
|
||||||
|
</div>
|
||||||
|
<!-- Card 6 -->
|
||||||
|
<div class="kanban-card bg-white p-4 rounded-lg border border-slate-200/60 shadow-sm cursor-grab active:cursor-grabbing hover:shadow transition-all" draggable="true" id="task-6">
|
||||||
|
<span class="text-[9px] font-semibold text-slate-400 block mb-1">FASE 1</span>
|
||||||
|
<p class="text-xs font-medium text-slate-800">Setup Repository & Strumenti</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="text-center text-xs text-slate-400 py-10">
|
||||||
|
Questa è la tua dashboard privata — non condividere il link.
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- SCRIPT DI LOGICA INTERATTIVA -->
|
||||||
|
<script>
|
||||||
|
// Accordion Control
|
||||||
|
function toggleAccordion(id) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
const arrow = document.getElementById('compreso-arrow');
|
||||||
|
el.classList.toggle('hidden');
|
||||||
|
arrow.classList.toggle('rotate-180');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swapping delle viste (Timeline / Kanban)
|
||||||
|
const btnTimeline = document.getElementById('btn-timeline');
|
||||||
|
const btnKanban = document.getElementById('btn-kanban');
|
||||||
|
const viewTimeline = document.getElementById('view-timeline');
|
||||||
|
const viewKanban = document.getElementById('view-kanban');
|
||||||
|
|
||||||
|
btnTimeline.addEventListener('click', () => {
|
||||||
|
viewKanban.classList.add('hidden');
|
||||||
|
viewTimeline.classList.remove('hidden');
|
||||||
|
btnTimeline.className = "px-5 py-1.5 bg-white rounded-md text-[11px] font-semibold text-slate-800 shadow-sm transition-all duration-200";
|
||||||
|
btnKanban.className = "px-5 py-1.5 rounded-md text-[11px] font-semibold text-slate-500 hover:text-slate-800 transition-all duration-200";
|
||||||
|
});
|
||||||
|
|
||||||
|
btnKanban.addEventListener('click', () => {
|
||||||
|
viewTimeline.classList.add('hidden');
|
||||||
|
viewKanban.classList.remove('hidden');
|
||||||
|
btnKanban.className = "px-5 py-1.5 bg-white rounded-md text-[11px] font-semibold text-slate-800 shadow-sm transition-all duration-200";
|
||||||
|
btnTimeline.className = "px-5 py-1.5 rounded-md text-[11px] font-semibold text-slate-500 hover:text-slate-800 transition-all duration-200";
|
||||||
|
|
||||||
|
// Inizializza Drag & Drop quando viene aperta la vista Kanban
|
||||||
|
initializeKanbanDragAndDrop();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logica Drag & Drop per il Kanban
|
||||||
|
function initializeKanbanDragAndDrop() {
|
||||||
|
const cards = document.querySelectorAll('.kanban-card');
|
||||||
|
const columns = document.querySelectorAll('.kanban-column');
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
card.addEventListener('dragstart', () => {
|
||||||
|
card.classList.add('dragging');
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('dragend', () => {
|
||||||
|
card.classList.remove('dragging');
|
||||||
|
updateCounts();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
columns.forEach(column => {
|
||||||
|
const dropzone = column.querySelector('.cards-dropzone');
|
||||||
|
|
||||||
|
column.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
column.classList.add('drag-over-zone');
|
||||||
|
});
|
||||||
|
|
||||||
|
column.addEventListener('dragleave', () => {
|
||||||
|
column.classList.remove('drag-over-zone');
|
||||||
|
});
|
||||||
|
|
||||||
|
column.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
column.classList.remove('drag-over-zone');
|
||||||
|
const draggingCard = document.querySelector('.dragging');
|
||||||
|
if (draggingCard) {
|
||||||
|
dropzone.appendChild(draggingCard);
|
||||||
|
|
||||||
|
// Se lanciato nella colonna "Fatto" (done), triggera una micro-celebrazione
|
||||||
|
if (column.getAttribute('data-status') === 'done' && typeof confetti === 'function') {
|
||||||
|
confetti({
|
||||||
|
particleCount: 50,
|
||||||
|
spread: 50,
|
||||||
|
origin: { y: 0.8 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateCounts();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggiornamento dei contatori delle colonne del Kanban
|
||||||
|
function updateCounts() {
|
||||||
|
const columns = document.querySelectorAll('.kanban-column');
|
||||||
|
columns.forEach(col => {
|
||||||
|
const countPill = col.querySelector('.count-pill');
|
||||||
|
const count = col.querySelector('.cards-dropzone').children.length;
|
||||||
|
countPill.innerText = count;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Lead Pipeline — Luxury Redesign</title>
|
||||||
|
<!-- Google Fonts: Plus Jakarta Sans -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<!-- Tailwind CSS CDN -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Plus Jakarta Sans', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: {
|
||||||
|
dark: '#1A463C', /* Verde scuro richiesto */
|
||||||
|
darkHover: '#13342D', /* Variante scura per hover */
|
||||||
|
active: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
bg: '#F8F9FA',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* Transizione fluida per l'apertura/chiusura della sidebar */
|
||||||
|
.sidebar-transition {
|
||||||
|
transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1), transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
/* Stile per l'effetto di trascinamento nel Kanban */
|
||||||
|
.dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
.drag-over {
|
||||||
|
background-color: rgba(26, 70, 60, 0.03);
|
||||||
|
border-color: rgba(26, 70, 60, 0.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-brand-bg font-sans text-slate-800 antialiased min-h-screen flex overflow-x-hidden">
|
||||||
|
|
||||||
|
<!-- SIDEBAR -->
|
||||||
|
<aside id="sidebar" class="w-64 bg-brand-dark text-white flex flex-col justify-between p-6 border-r border-emerald-950/20 shrink-0 sidebar-transition relative z-10">
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<!-- Logo e Intestazione -->
|
||||||
|
<div class="mb-10 px-2 flex items-center justify-between">
|
||||||
|
<span id="sidebar-logo-text" class="text-lg font-bold tracking-wider text-emerald-50 whitespace-nowrap transition-opacity duration-300">iamcavalli</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigazione Principale -->
|
||||||
|
<nav class="space-y-1">
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2v-4zM14 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2v-4z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-white bg-brand-active font-medium transition-all duration-200 shadow-sm">
|
||||||
|
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Lead</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Clienti</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Progetti</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Preventivi</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M7 7h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Offerte</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Area Inferiore Sidebar -->
|
||||||
|
<div class="border-t border-white/10 pt-4 space-y-3 overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between px-3 py-1 text-xs text-emerald-200/50 whitespace-nowrap">
|
||||||
|
<span class="sidebar-text transition-opacity duration-300">Tema</span>
|
||||||
|
<button class="p-1 rounded-full bg-white/5 hover:bg-white/10 text-emerald-200 hover:text-white transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707M14 12a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-red-300/80 hover:text-red-200 hover:bg-red-950/20 transition-all duration-200 whitespace-nowrap">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
|
||||||
|
<span class="sidebar-text transition-opacity duration-300">Esci</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- SEZIONE PRINCIPALE -->
|
||||||
|
<div class="flex-1 flex flex-col min-w-0">
|
||||||
|
|
||||||
|
<!-- BARRA DI INTESTAZIONE SUPERIORE (Contiene i controlli globali) -->
|
||||||
|
<header class="h-16 border-b border-slate-100 bg-white px-8 flex items-center justify-between shrink-0">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<!-- Pulsante Minimal per Aprire/Chiudere la Sidebar -->
|
||||||
|
<button id="sidebar-toggle" class="p-2 -ml-2 rounded-lg text-slate-500 hover:bg-slate-50 hover:text-slate-800 transition-colors focus:outline-none" title="Espandi/Comprimi Sidebar">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 6h16M4 12h12M4 18h16"/></svg>
|
||||||
|
</button>
|
||||||
|
<span class="text-xs text-slate-400 font-medium">Area di Lavoro</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="text-xs font-semibold text-slate-700 bg-slate-100 px-2.5 py-1 rounded-full">Demo Account</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- AREA DEL CONTENUTO DINAMICO -->
|
||||||
|
<main class="flex-1 p-8 lg:p-10 max-w-[1400px] w-full mx-auto flex flex-col gap-8 overflow-y-auto">
|
||||||
|
|
||||||
|
<!-- INTESTAZIONE SEZIONE -->
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between sm:items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-semibold text-slate-900 tracking-tight">Lead Pipeline</h1>
|
||||||
|
<p class="text-xs text-slate-400 mt-1">Gestisci e monitora i tuoi contatti commerciali</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pulsante Nuovo Lead -->
|
||||||
|
<button class="bg-brand-dark hover:bg-brand-darkHover text-white text-xs font-medium tracking-wide px-5 py-3 rounded-lg flex items-center gap-2 transition-all duration-200 shadow-sm self-start sm:self-auto">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||||
|
Nuovo Lead
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AZIONI: BARRA DI RICERCA & SELETTORE VISTA -->
|
||||||
|
<div class="flex flex-col md:flex-row justify-between md:items-center gap-4">
|
||||||
|
<!-- Barra di ricerca -->
|
||||||
|
<div class="relative w-full md:w-80">
|
||||||
|
<span class="absolute inset-y-0 left-0 flex items-center pl-3.5 pointer-events-none text-slate-400">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Cerca lead..."
|
||||||
|
class="w-full pl-10 pr-4 py-2.5 bg-white text-sm text-slate-800 placeholder-slate-400 border border-slate-200/80 rounded-lg focus:outline-none focus:border-brand-dark focus:ring-1 focus:ring-brand-dark transition-all duration-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selettore Vista (Lista vs Kanban) -->
|
||||||
|
<div class="bg-slate-200/50 p-1 rounded-lg flex gap-1 self-start md:self-auto">
|
||||||
|
<button id="btn-view-list" class="px-5 py-1.5 rounded-md text-xs font-medium bg-white text-slate-800 shadow-sm transition-all duration-200">
|
||||||
|
Lista
|
||||||
|
</button>
|
||||||
|
<button id="btn-view-kanban" class="px-5 py-1.5 rounded-md text-xs font-medium text-slate-500 hover:text-slate-800 transition-all duration-200">
|
||||||
|
Kanban
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VISTA 1: TABELLA (Default) -->
|
||||||
|
<div id="view-list" class="bg-white rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] overflow-hidden transition-opacity duration-300">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-slate-100/80 bg-slate-50/50 text-slate-400 text-[11px] font-semibold uppercase tracking-wider">
|
||||||
|
<th class="py-4 px-6">Nome</th>
|
||||||
|
<th class="py-4 px-6">Email</th>
|
||||||
|
<th class="py-4 px-6">Telefono</th>
|
||||||
|
<th class="py-4 px-6">Azienda</th>
|
||||||
|
<th class="py-4 px-6 text-center">Stato</th>
|
||||||
|
<th class="py-4 px-6">Prossima Azione</th>
|
||||||
|
<th class="py-4 px-6 text-center">Tag</th>
|
||||||
|
<th class="py-4 px-6 text-right">Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100/60 text-sm">
|
||||||
|
<tr class="hover:bg-slate-50/30 transition-all duration-150">
|
||||||
|
<td class="py-4 px-6 font-medium text-slate-900">Nome Cognome</td>
|
||||||
|
<td class="py-4 px-6 text-slate-500 text-xs">test@gmail.com</td>
|
||||||
|
<td class="py-4 px-6 text-slate-500 text-xs font-mono">+39 444 3322111</td>
|
||||||
|
<td class="py-4 px-6 text-slate-600">Azienda Srl</td>
|
||||||
|
<td class="py-4 px-6 text-center">
|
||||||
|
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-semibold tracking-wide bg-purple-50 text-purple-600 border border-purple-100 uppercase">
|
||||||
|
qualified
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-slate-400">—</td>
|
||||||
|
<td class="py-4 px-6 text-center">
|
||||||
|
<button class="inline-flex items-center justify-center w-6 h-6 rounded-full border border-dashed border-slate-200 text-slate-400 hover:text-slate-600 hover:border-slate-400 transition-all duration-200">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-right">
|
||||||
|
<a href="#" class="text-xs font-medium text-brand-dark hover:text-brand-darkHover inline-flex items-center gap-1 group transition-colors">
|
||||||
|
Dettagli
|
||||||
|
<svg class="w-3 h-3 transform group-hover:translate-x-0.5 transition-transform" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24"><path d="M8.25 4.5l7.5 7.5-7.5 7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-slate-100 px-6 py-4 flex items-center justify-between text-xs text-slate-400">
|
||||||
|
<span>Mostrando 1 di 1 lead</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VISTA 2: KANBAN (Nascosta all'avvio) -->
|
||||||
|
<div id="view-kanban" class="hidden grid grid-cols-1 md:grid-cols-4 gap-6 transition-opacity duration-300">
|
||||||
|
|
||||||
|
<!-- Colonna 1: Nuovo -->
|
||||||
|
<div class="kanban-column bg-slate-50/75 rounded-xl border border-slate-100 p-4 flex flex-col gap-4 min-h-[450px]" data-status="new">
|
||||||
|
<div class="flex items-center justify-between border-b border-slate-100 pb-2">
|
||||||
|
<span class="text-xs font-semibold uppercase tracking-wider text-slate-500">Nuovo</span>
|
||||||
|
<span class="text-xs font-medium bg-slate-200/60 text-slate-600 px-2 py-0.5 rounded-full count">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="cards-container flex-1 flex flex-col gap-3">
|
||||||
|
<!-- Esempio di lead spostabile -->
|
||||||
|
<div id="lead-card-2" class="kanban-card bg-white p-4 rounded-lg border border-slate-200/80 shadow-sm cursor-grab active:cursor-grabbing hover:border-brand-dark/30 hover:shadow-md transition-all duration-200" draggable="true">
|
||||||
|
<div class="flex justify-between items-start gap-2 mb-2">
|
||||||
|
<h4 class="text-xs font-semibold text-slate-800">Marco Rossi</h4>
|
||||||
|
<span class="text-[9px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-100 px-1.5 py-0.5 rounded uppercase">New</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-[11px] text-slate-400 mb-3">rossi@azienda.it</p>
|
||||||
|
<div class="flex justify-between items-center text-[10px] text-slate-500">
|
||||||
|
<span class="font-medium">Rossi Consulting</span>
|
||||||
|
<span>Tag +</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonna 2: Contattato -->
|
||||||
|
<div class="kanban-column bg-slate-50/75 rounded-xl border border-slate-100 p-4 flex flex-col gap-4 min-h-[450px]" data-status="contacted">
|
||||||
|
<div class="flex items-center justify-between border-b border-slate-100 pb-2">
|
||||||
|
<span class="text-xs font-semibold uppercase tracking-wider text-slate-500">Contattato</span>
|
||||||
|
<span class="text-xs font-medium bg-slate-200/60 text-slate-600 px-2 py-0.5 rounded-full count">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="cards-container flex-1 flex flex-col gap-3"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonna 3: Qualificato -->
|
||||||
|
<div class="kanban-column bg-slate-50/75 rounded-xl border border-slate-100 p-4 flex flex-col gap-4 min-h-[450px]" data-status="qualified">
|
||||||
|
<div class="flex items-center justify-between border-b border-slate-100 pb-2">
|
||||||
|
<span class="text-xs font-semibold uppercase tracking-wider text-slate-500">Qualificato</span>
|
||||||
|
<span class="text-xs font-medium bg-slate-200/60 text-slate-600 px-2 py-0.5 rounded-full count">1</span>
|
||||||
|
</div>
|
||||||
|
<div class="cards-container flex-1 flex flex-col gap-3">
|
||||||
|
|
||||||
|
<!-- Lead proveniente dalla lista originale -->
|
||||||
|
<div id="lead-card-1" class="kanban-card bg-white p-4 rounded-lg border border-slate-200/80 shadow-sm cursor-grab active:cursor-grabbing hover:border-brand-dark/30 hover:shadow-md transition-all duration-200" draggable="true">
|
||||||
|
<div class="flex justify-between items-start gap-2 mb-2">
|
||||||
|
<h4 class="text-xs font-semibold text-slate-800">Nome Cognome</h4>
|
||||||
|
<span class="text-[9px] font-semibold bg-purple-50 text-purple-600 border border-purple-100 px-1.5 py-0.5 rounded uppercase">Qualified</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-[11px] text-slate-400 mb-3">test@gmail.com</p>
|
||||||
|
<div class="flex justify-between items-center text-[10px] text-slate-500">
|
||||||
|
<span class="font-medium">Azienda Srl</span>
|
||||||
|
<span>Tag +</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonna 4: In Trattativa -->
|
||||||
|
<div class="kanban-column bg-slate-50/75 rounded-xl border border-slate-100 p-4 flex flex-col gap-4 min-h-[450px]" data-status="negotiation">
|
||||||
|
<div class="flex items-center justify-between border-b border-slate-100 pb-2">
|
||||||
|
<span class="text-xs font-semibold uppercase tracking-wider text-slate-500">Trattativa</span>
|
||||||
|
<span class="text-xs font-medium bg-slate-200/60 text-slate-600 px-2 py-0.5 rounded-full count">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="cards-container flex-1 flex flex-col gap-3"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SCRIPT LOGICA (Gestione Sidebar e Kanban) -->
|
||||||
|
<script>
|
||||||
|
// --- GESTIONE SIDEBAR COLLASSABILE ---
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const toggleButton = document.getElementById('sidebar-toggle');
|
||||||
|
const logoText = document.getElementById('sidebar-logo-text');
|
||||||
|
const sidebarTexts = document.querySelectorAll('.sidebar-text');
|
||||||
|
|
||||||
|
toggleButton.addEventListener('click', () => {
|
||||||
|
// Verifica lo stato attuale
|
||||||
|
const isCollapsed = sidebar.classList.contains('w-20');
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
// Espandi
|
||||||
|
sidebar.classList.remove('w-20');
|
||||||
|
sidebar.classList.add('w-64');
|
||||||
|
setTimeout(() => {
|
||||||
|
logoText.classList.remove('opacity-0');
|
||||||
|
sidebarTexts.forEach(text => text.classList.remove('opacity-0'));
|
||||||
|
}, 150);
|
||||||
|
} else {
|
||||||
|
// Collassa
|
||||||
|
logoText.classList.add('opacity-0');
|
||||||
|
sidebarTexts.forEach(text => text.classList.add('opacity-0'));
|
||||||
|
sidebar.classList.remove('w-64');
|
||||||
|
sidebar.classList.add('w-20');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- INTERRUTTORE DI VISTA (LISTA / KANBAN) ---
|
||||||
|
const btnViewList = document.getElementById('btn-view-list');
|
||||||
|
const btnViewKanban = document.getElementById('btn-view-kanban');
|
||||||
|
const viewList = document.getElementById('view-list');
|
||||||
|
const viewKanban = document.getElementById('view-kanban');
|
||||||
|
|
||||||
|
btnViewList.addEventListener('click', () => {
|
||||||
|
// Attiva vista Lista
|
||||||
|
viewKanban.classList.add('hidden');
|
||||||
|
viewList.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Regola stili pulsanti
|
||||||
|
btnViewList.className = "px-5 py-1.5 rounded-md text-xs font-medium bg-white text-slate-800 shadow-sm transition-all duration-200";
|
||||||
|
btnViewKanban.className = "px-5 py-1.5 rounded-md text-xs font-medium text-slate-500 hover:text-slate-800 transition-all duration-200";
|
||||||
|
});
|
||||||
|
|
||||||
|
btnViewKanban.addEventListener('click', () => {
|
||||||
|
// Attiva vista Kanban
|
||||||
|
viewList.classList.add('hidden');
|
||||||
|
viewKanban.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Regola stili pulsanti
|
||||||
|
btnViewKanban.className = "px-5 py-1.5 rounded-md text-xs font-medium bg-white text-slate-800 shadow-sm transition-all duration-200";
|
||||||
|
btnViewList.className = "px-5 py-1.5 rounded-md text-xs font-medium text-slate-500 hover:text-slate-800 transition-all duration-200";
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- FUNZIONALITÀ DRAG AND DROP KANBAN ---
|
||||||
|
const cards = document.querySelectorAll('.kanban-card');
|
||||||
|
const columns = document.querySelectorAll('.kanban-column');
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
card.addEventListener('dragstart', () => {
|
||||||
|
card.classList.add('dragging');
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('dragend', () => {
|
||||||
|
card.classList.remove('dragging');
|
||||||
|
updateColumnCounts();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
columns.forEach(column => {
|
||||||
|
const container = column.querySelector('.cards-container');
|
||||||
|
|
||||||
|
column.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
column.classList.add('drag-over');
|
||||||
|
});
|
||||||
|
|
||||||
|
column.addEventListener('dragleave', () => {
|
||||||
|
column.classList.remove('drag-over');
|
||||||
|
});
|
||||||
|
|
||||||
|
column.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
column.classList.remove('drag-over');
|
||||||
|
|
||||||
|
const draggingCard = document.querySelector('.dragging');
|
||||||
|
if (draggingCard) {
|
||||||
|
container.appendChild(draggingCard);
|
||||||
|
|
||||||
|
// Cambia visivamente il badge interno dello stato in base alla destinazione
|
||||||
|
const badge = draggingCard.querySelector('span');
|
||||||
|
const status = column.getAttribute('data-status');
|
||||||
|
|
||||||
|
if (status === 'new') {
|
||||||
|
badge.innerText = 'New';
|
||||||
|
badge.className = 'text-[9px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-100 px-1.5 py-0.5 rounded uppercase';
|
||||||
|
} else if (status === 'contacted') {
|
||||||
|
badge.innerText = 'Contacted';
|
||||||
|
badge.className = 'text-[9px] font-semibold bg-blue-50 text-blue-600 border border-blue-100 px-1.5 py-0.5 rounded uppercase';
|
||||||
|
} else if (status === 'qualified') {
|
||||||
|
badge.innerText = 'Qualified';
|
||||||
|
badge.className = 'text-[9px] font-semibold bg-purple-50 text-purple-600 border border-purple-100 px-1.5 py-0.5 rounded uppercase';
|
||||||
|
} else if (status === 'negotiation') {
|
||||||
|
badge.innerText = 'In Trattativa';
|
||||||
|
badge.className = 'text-[9px] font-semibold bg-amber-50 text-amber-600 border border-amber-100 px-1.5 py-0.5 rounded uppercase';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Funzione d'appoggio per aggiornare il conteggio numerico nelle colonne
|
||||||
|
function updateColumnCounts() {
|
||||||
|
columns.forEach(column => {
|
||||||
|
const countBadge = column.querySelector('.count');
|
||||||
|
const cardCount = column.querySelector('.cards-container').children.length;
|
||||||
|
countBadge.innerText = cardCount;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Esegui inizialmente per impostare i corretti conteggi
|
||||||
|
updateColumnCounts();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Offerte — Luxury Admin CRM</title>
|
||||||
|
<!-- Google Fonts: Plus Jakarta Sans -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<!-- Tailwind CSS CDN -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Plus Jakarta Sans', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: {
|
||||||
|
dark: '#1A463C', /* Verde scuro richiesto */
|
||||||
|
darkHover: '#13342D', /* Variante scura per hover */
|
||||||
|
active: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
bg: '#F8F9FA',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.sidebar-transition {
|
||||||
|
transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1), transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-brand-bg font-sans text-slate-800 antialiased min-h-screen flex overflow-x-hidden">
|
||||||
|
|
||||||
|
<!-- SIDEBAR -->
|
||||||
|
<aside id="sidebar" class="w-64 bg-brand-dark text-white flex flex-col justify-between p-6 border-r border-emerald-950/20 shrink-0 sidebar-transition relative z-10">
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<!-- Logo e Intestazione -->
|
||||||
|
<div class="mb-10 px-2 flex items-center justify-between">
|
||||||
|
<span id="sidebar-logo-text" class="text-lg font-bold tracking-wider text-emerald-50 whitespace-nowrap transition-opacity duration-300">iamcavalli</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigazione Principale -->
|
||||||
|
<nav class="space-y-1">
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2v-4zM14 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2v-4z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Lead</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Clienti</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Progetti</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Preventivi</span>
|
||||||
|
</a>
|
||||||
|
<!-- Stato Attivo su Offerte -->
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-white bg-brand-active font-medium transition-all duration-200 shadow-sm">
|
||||||
|
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M7 7h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Offerte</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Area Inferiore Sidebar -->
|
||||||
|
<div class="border-t border-white/10 pt-4 space-y-3 overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between px-3 py-1 text-xs text-emerald-200/50 whitespace-nowrap">
|
||||||
|
<span class="sidebar-text transition-opacity duration-300">Tema</span>
|
||||||
|
<button class="p-1 rounded-full bg-white/5 hover:bg-white/10 text-emerald-200 hover:text-white transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707M14 12a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-red-300/80 hover:text-red-200 hover:bg-red-950/20 transition-all duration-200 whitespace-nowrap">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
|
||||||
|
<span class="sidebar-text transition-opacity duration-300">Esci</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- SEZIONE PRINCIPALE -->
|
||||||
|
<div class="flex-1 flex flex-col min-w-0">
|
||||||
|
|
||||||
|
<!-- BARRA DI INTESTAZIONE SUPERIORE -->
|
||||||
|
<header class="h-16 border-b border-slate-100 bg-white px-8 flex items-center justify-between shrink-0">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<!-- Pulsante Apri/Chiudi Sidebar -->
|
||||||
|
<button id="sidebar-toggle" class="p-2 -ml-2 rounded-lg text-slate-500 hover:bg-slate-50 hover:text-slate-800 transition-colors focus:outline-none" title="Espandi/Comprimi Sidebar">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 6h16M4 12h12M4 18h16"/></svg>
|
||||||
|
</button>
|
||||||
|
<span class="text-xs text-slate-400 font-medium">Area di Lavoro</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="text-xs font-semibold text-slate-700 bg-slate-100 px-2.5 py-1 rounded-full">Demo Account</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- AREA DEL CONTENUTO -->
|
||||||
|
<main class="flex-1 p-8 lg:p-10 max-w-[1400px] w-full mx-auto flex flex-col gap-8 overflow-y-auto">
|
||||||
|
|
||||||
|
<!-- INTESTAZIONE SEZIONE -->
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between sm:items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-semibold text-slate-900 tracking-tight">Offerte</h1>
|
||||||
|
<p class="text-xs text-slate-400 mt-1">Configura e gestisci i pacchetti di offerta commerciali</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottone Nuova Offerta -->
|
||||||
|
<button class="bg-brand-dark hover:bg-brand-darkHover text-white text-xs font-medium tracking-wide px-5 py-3 rounded-lg flex items-center gap-2 transition-all duration-200 shadow-sm self-start sm:self-auto">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||||
|
Nuova Offerta
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BARRA FILTRI -->
|
||||||
|
<div class="flex flex-col lg:flex-row justify-between lg:items-center gap-4">
|
||||||
|
<!-- Selettori Categorie di Offerta -->
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button class="px-4 py-2 text-xs font-medium bg-brand-dark text-white rounded-lg transition-colors">
|
||||||
|
Tutti
|
||||||
|
</button>
|
||||||
|
<button class="px-4 py-2 text-xs font-medium bg-white text-slate-600 hover:text-slate-900 hover:bg-slate-100/70 border border-slate-200/60 rounded-lg transition-all duration-200">
|
||||||
|
Entry Offer
|
||||||
|
</button>
|
||||||
|
<button class="px-4 py-2 text-xs font-medium bg-white text-slate-600 hover:text-slate-900 hover:bg-slate-100/70 border border-slate-200/60 rounded-lg transition-all duration-200">
|
||||||
|
Retainer Offer
|
||||||
|
</button>
|
||||||
|
<button class="px-4 py-2 text-xs font-medium bg-white text-slate-600 hover:text-slate-900 hover:bg-slate-100/70 border border-slate-200/60 rounded-lg transition-all duration-200">
|
||||||
|
Signature Offer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Checkbox Opzione Archiviati -->
|
||||||
|
<label class="flex items-center gap-2.5 cursor-pointer group text-xs text-slate-500 hover:text-slate-800 transition-colors">
|
||||||
|
<input type="checkbox" class="w-4 h-4 rounded text-brand-dark border-slate-300 focus:ring-brand-dark focus:ring-opacity-25 transition-all" />
|
||||||
|
<span class="select-none font-medium">Mostra offerte archiviate</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- GRID CARDS DELLE OFFERTE -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
|
||||||
|
<!-- Card 1: Mantenimento -->
|
||||||
|
<div class="bg-white p-6 rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] hover:shadow-[0_8px_30px_rgba(0,0,0,0.02)] transition-all duration-200 flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-start gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-900">Mantenimento</h3>
|
||||||
|
<p class="text-[11px] text-slate-400 mt-0.5">(nei clienti già attivi)</p>
|
||||||
|
</div>
|
||||||
|
<span class="px-2 py-1 rounded text-[9px] font-semibold bg-blue-50 text-blue-600 border border-blue-100 uppercase tracking-wider">
|
||||||
|
Retainer Offer
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Listino Tabellare Opzioni -->
|
||||||
|
<div class="border border-slate-100 rounded-lg overflow-hidden my-6">
|
||||||
|
<table class="w-full text-[11px]">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50 border-b border-slate-100 text-slate-400 font-semibold uppercase">
|
||||||
|
<th class="py-2 px-3 text-left">Tier</th>
|
||||||
|
<th class="py-2 px-3 text-right">Servizi</th>
|
||||||
|
<th class="py-2 px-3 text-right">Pubblico</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100 text-slate-600 font-mono">
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-3 font-semibold text-slate-800">A</td>
|
||||||
|
<td class="py-2 px-3 text-right">€200,00</td>
|
||||||
|
<td class="py-2 px-3 text-right text-slate-400">€200,00</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-3 font-semibold text-slate-800">B</td>
|
||||||
|
<td class="py-2 px-3 text-right">€300,00</td>
|
||||||
|
<td class="py-2 px-3 text-right text-slate-400">€300,00</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-3 font-semibold text-slate-800">C</td>
|
||||||
|
<td class="py-2 px-3 text-right">€400,00</td>
|
||||||
|
<td class="py-2 px-3 text-right text-slate-400">€400,00</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 2: Sblocca Business -->
|
||||||
|
<div class="bg-white p-6 rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] hover:shadow-[0_8px_30px_rgba(0,0,0,0.02)] transition-all duration-200 flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-start gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-900">Sblocca Business</h3>
|
||||||
|
<p class="text-[11px] text-slate-400 mt-0.5">Avviamento strategico</p>
|
||||||
|
</div>
|
||||||
|
<span class="px-2 py-1 rounded text-[9px] font-semibold bg-purple-50 text-purple-600 border border-purple-100 uppercase tracking-wider">
|
||||||
|
Entry Offer
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Listino Tabellare Opzioni -->
|
||||||
|
<div class="border border-slate-100 rounded-lg overflow-hidden my-6">
|
||||||
|
<table class="w-full text-[11px]">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50 border-b border-slate-100 text-slate-400 font-semibold uppercase">
|
||||||
|
<th class="py-2 px-3 text-left">Tier</th>
|
||||||
|
<th class="py-2 px-3 text-right">Servizi</th>
|
||||||
|
<th class="py-2 px-3 text-right">Pubblico</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100 text-slate-600 font-mono">
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-3 font-semibold text-slate-800">A</td>
|
||||||
|
<td class="py-2 px-3 text-right">€2.600,00</td>
|
||||||
|
<td class="py-2 px-3 text-right text-slate-400">€400,00</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-3 font-semibold text-slate-800">B</td>
|
||||||
|
<td class="py-2 px-3 text-right">€3.300,00</td>
|
||||||
|
<td class="py-2 px-3 text-right text-slate-400">€600,00</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-3 font-semibold text-slate-800">C</td>
|
||||||
|
<td class="py-2 px-3 text-right">€3.900,00</td>
|
||||||
|
<td class="py-2 px-3 text-right text-slate-400">€1.200,00</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 3: Web Domination -->
|
||||||
|
<div class="bg-white p-6 rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] hover:shadow-[0_8px_30px_rgba(0,0,0,0.02)] transition-all duration-200 flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-start gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-900">Web Domination</h3>
|
||||||
|
<p class="text-[11px] text-slate-400 mt-0.5">Soluzione enterprise omnicanale</p>
|
||||||
|
</div>
|
||||||
|
<span class="px-2 py-1 rounded text-[9px] font-semibold bg-amber-50 text-amber-700 border border-amber-100 uppercase tracking-wider">
|
||||||
|
Signature Offer
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Listino Tabellare Opzioni -->
|
||||||
|
<div class="border border-slate-100 rounded-lg overflow-hidden my-6">
|
||||||
|
<table class="w-full text-[11px]">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50 border-b border-slate-100 text-slate-400 font-semibold uppercase">
|
||||||
|
<th class="py-2 px-3 text-left">Tier</th>
|
||||||
|
<th class="py-2 px-3 text-right">Servizi</th>
|
||||||
|
<th class="py-2 px-3 text-right">Pubblico</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100 text-slate-600 font-mono">
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-3 font-semibold text-slate-800">A</td>
|
||||||
|
<td class="py-2 px-3 text-right">€9.800,00</td>
|
||||||
|
<td class="py-2 px-3 text-right text-slate-400">€6.000,00</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-3 font-semibold text-slate-800">B</td>
|
||||||
|
<td class="py-2 px-3 text-right">€20.250,00</td>
|
||||||
|
<td class="py-2 px-3 text-right text-slate-400">€9.000,00</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-3 font-semibold text-slate-800">C</td>
|
||||||
|
<td class="py-2 px-3 text-right">€25.250,00</td>
|
||||||
|
<td class="py-2 px-3 text-right text-slate-400">€12.000,00</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 4: Business in Banca -->
|
||||||
|
<div class="bg-white p-6 rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] hover:shadow-[0_8px_30px_rgba(0,0,0,0.02)] transition-all duration-200 flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-start gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-900">Business in Banca</h3>
|
||||||
|
<p class="text-[11px] text-slate-400 mt-0.5">Controllo tesoreria integrato</p>
|
||||||
|
</div>
|
||||||
|
<span class="px-2 py-1 rounded text-[9px] font-semibold bg-blue-50 text-blue-600 border border-blue-100 uppercase tracking-wider">
|
||||||
|
Retainer Offer
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Listino Tabellare Opzioni -->
|
||||||
|
<div class="border border-slate-100 rounded-lg overflow-hidden my-6">
|
||||||
|
<table class="w-full text-[11px]">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50 border-b border-slate-100 text-slate-400 font-semibold uppercase">
|
||||||
|
<th class="py-2 px-3 text-left">Tier</th>
|
||||||
|
<th class="py-2 px-3 text-right">Servizi</th>
|
||||||
|
<th class="py-2 px-3 text-right">Pubblico</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100 text-slate-600 font-mono">
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-3 font-semibold text-slate-800">A</td>
|
||||||
|
<td class="py-2 px-3 text-right">€200,00</td>
|
||||||
|
<td class="py-2 px-3 text-right text-slate-400">€200,00</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-3 font-semibold text-slate-800">B</td>
|
||||||
|
<td class="py-2 px-3 text-right">€400,00</td>
|
||||||
|
<td class="py-2 px-3 text-right text-slate-400">€300,00</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-3 font-semibold text-slate-800">C</td>
|
||||||
|
<td class="py-2 px-3 text-right">€900,00</td>
|
||||||
|
<td class="py-2 px-3 text-right text-slate-400">€500,00</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SCRIPT DI LOGICA (Gestione Sidebar) -->
|
||||||
|
<script>
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const toggleButton = document.getElementById('sidebar-toggle');
|
||||||
|
const logoText = document.getElementById('sidebar-logo-text');
|
||||||
|
const sidebarTexts = document.querySelectorAll('.sidebar-text');
|
||||||
|
|
||||||
|
toggleButton.addEventListener('click', () => {
|
||||||
|
const isCollapsed = sidebar.classList.contains('w-20');
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
sidebar.classList.remove('w-20');
|
||||||
|
sidebar.classList.add('w-64');
|
||||||
|
setTimeout(() => {
|
||||||
|
logoText.classList.remove('opacity-0');
|
||||||
|
sidebarTexts.forEach(text => text.classList.remove('opacity-0'));
|
||||||
|
}, 150);
|
||||||
|
} else {
|
||||||
|
logoText.classList.add('opacity-0');
|
||||||
|
sidebarTexts.forEach(text => text.classList.add('opacity-0'));
|
||||||
|
sidebar.classList.remove('w-64');
|
||||||
|
sidebar.classList.add('w-20');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Preventivi — Luxury Admin CRM</title>
|
||||||
|
<!-- Google Fonts: Plus Jakarta Sans -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<!-- Tailwind CSS CDN -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Plus Jakarta Sans', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: {
|
||||||
|
dark: '#1A463C', /* Verde scuro richiesto */
|
||||||
|
darkHover: '#13342D', /* Variante scura per hover */
|
||||||
|
active: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
bg: '#F8F9FA',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.sidebar-transition {
|
||||||
|
transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1), transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-brand-bg font-sans text-slate-800 antialiased min-h-screen flex overflow-x-hidden">
|
||||||
|
|
||||||
|
<!-- SIDEBAR -->
|
||||||
|
<aside id="sidebar" class="w-64 bg-brand-dark text-white flex flex-col justify-between p-6 border-r border-emerald-950/20 shrink-0 sidebar-transition relative z-10">
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<!-- Logo e Intestazione -->
|
||||||
|
<div class="mb-10 px-2 flex items-center justify-between">
|
||||||
|
<span id="sidebar-logo-text" class="text-lg font-bold tracking-wider text-emerald-50 whitespace-nowrap transition-opacity duration-300">iamcavalli</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigazione Principale -->
|
||||||
|
<nav class="space-y-1">
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2H6a2 2 0 01-2-2v-4zM14 16a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 01-2-2v-4z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Lead</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Clienti</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Progetti</span>
|
||||||
|
</a>
|
||||||
|
<!-- Stato Attivo su Preventivi -->
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-white bg-brand-active font-medium transition-all duration-200 shadow-sm">
|
||||||
|
<svg class="w-4 h-4 shrink-0 text-emerald-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Preventivi</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-emerald-200/70 hover:text-white hover:bg-white/5 transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M7 7h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||||
|
<span class="sidebar-text whitespace-nowrap transition-opacity duration-300">Offerte</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Area Inferiore Sidebar -->
|
||||||
|
<div class="border-t border-white/10 pt-4 space-y-3 overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between px-3 py-1 text-xs text-emerald-200/50 whitespace-nowrap">
|
||||||
|
<span class="sidebar-text transition-opacity duration-300">Tema</span>
|
||||||
|
<button class="p-1 rounded-full bg-white/5 hover:bg-white/10 text-emerald-200 hover:text-white transition-all duration-200">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707M14 12a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-red-300/80 hover:text-red-200 hover:bg-red-950/20 transition-all duration-200 whitespace-nowrap">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
|
||||||
|
<span class="sidebar-text transition-opacity duration-300">Esci</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- SEZIONE PRINCIPALE -->
|
||||||
|
<div class="flex-1 flex flex-col min-w-0">
|
||||||
|
|
||||||
|
<!-- BARRA DI INTESTAZIONE SUPERIORE -->
|
||||||
|
<header class="h-16 border-b border-slate-100 bg-white px-8 flex items-center justify-between shrink-0">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<!-- Pulsante Apri/Chiudi Sidebar -->
|
||||||
|
<button id="sidebar-toggle" class="p-2 -ml-2 rounded-lg text-slate-500 hover:bg-slate-50 hover:text-slate-800 transition-colors focus:outline-none" title="Espandi/Comprimi Sidebar">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 6h16M4 12h12M4 18h16"/></svg>
|
||||||
|
</button>
|
||||||
|
<span class="text-xs text-slate-400 font-medium">Area di Lavoro</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="text-xs font-semibold text-slate-700 bg-slate-100 px-2.5 py-1 rounded-full">Demo Account</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- AREA DEL CONTENUTO -->
|
||||||
|
<main class="flex-1 p-8 lg:p-10 max-w-[1400px] w-full mx-auto flex flex-col gap-8 overflow-y-auto">
|
||||||
|
|
||||||
|
<!-- INTESTAZIONE SEZIONE -->
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between sm:items-start gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-semibold text-slate-900 tracking-tight">Preventivi</h1>
|
||||||
|
<p class="text-xs text-slate-400 mt-1">Preventivi generati con AI e pubblicati ai clienti</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pulsante Genera -->
|
||||||
|
<button class="bg-brand-dark hover:bg-brand-darkHover text-white text-xs font-medium tracking-wide px-5 py-3 rounded-lg flex items-center gap-2 transition-all duration-200 shadow-sm self-start sm:self-auto">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||||
|
Genera preventivo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TABELLA PREVENTIVI -->
|
||||||
|
<div class="bg-white rounded-xl border border-slate-100 shadow-[0_4px_20px_rgba(0,0,0,0.01)] overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-slate-100/80 bg-slate-50/50 text-slate-400 text-[11px] font-semibold uppercase tracking-wider">
|
||||||
|
<th class="py-4 px-6 w-1/3">Titolo</th>
|
||||||
|
<th class="py-4 px-6">Lead / Cliente</th>
|
||||||
|
<th class="py-4 px-6">Offerta</th>
|
||||||
|
<th class="py-4 px-6 text-center">Stato</th>
|
||||||
|
<th class="py-4 px-6">Data</th>
|
||||||
|
<th class="py-4 px-6 text-right">Azione</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100/60 text-sm">
|
||||||
|
|
||||||
|
<!-- Riga 1: Pubblicato -->
|
||||||
|
<tr class="hover:bg-slate-50/30 transition-all duration-150">
|
||||||
|
<td class="py-4 px-6 font-semibold text-slate-900">
|
||||||
|
Rossi Inc × Entry Offer
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-slate-600">
|
||||||
|
Mario Rossi
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-slate-500 font-medium">
|
||||||
|
Entry Offer
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-center">
|
||||||
|
<!-- Badge Stato: Pubblicato -->
|
||||||
|
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-semibold tracking-wide bg-sky-50 text-sky-700 border border-sky-100 uppercase">
|
||||||
|
Pubblicato
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-slate-500 text-xs font-mono">
|
||||||
|
26/06/2026
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-right">
|
||||||
|
<a href="#" class="text-xs font-medium text-brand-dark hover:text-brand-darkHover inline-flex items-center gap-1.5 group transition-colors">
|
||||||
|
Apri
|
||||||
|
<svg class="w-3.5 h-3.5 transform group-hover:translate-x-0.5 transition-transform" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24"><path d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Riga 2: Accettato -->
|
||||||
|
<tr class="hover:bg-slate-50/30 transition-all duration-150">
|
||||||
|
<td class="py-4 px-6 font-semibold text-slate-900">
|
||||||
|
Rossi Inc × Entry Offer
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-slate-600">
|
||||||
|
Mario Rossi
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-slate-500 font-medium">
|
||||||
|
Entry Offer
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-center">
|
||||||
|
<!-- Badge Stato: Accettato -->
|
||||||
|
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-semibold tracking-wide bg-emerald-50 text-emerald-700 border border-emerald-100 uppercase">
|
||||||
|
Accettato
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-slate-500 text-xs font-mono">
|
||||||
|
20/06/2026
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-right">
|
||||||
|
<a href="#" class="text-xs font-medium text-brand-dark hover:text-brand-darkHover inline-flex items-center gap-1.5 group transition-colors">
|
||||||
|
Apri
|
||||||
|
<svg class="w-3.5 h-3.5 transform group-hover:translate-x-0.5 transition-transform" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24"><path d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Tabella -->
|
||||||
|
<div class="border-t border-slate-100 px-6 py-4 flex items-center justify-between text-xs text-slate-400">
|
||||||
|
<span>Mostrando 2 di 2 preventivi generati</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SCRIPT DI LOGICA (Gestione Sidebar) -->
|
||||||
|
<script>
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const toggleButton = document.getElementById('sidebar-toggle');
|
||||||
|
const logoText = document.getElementById('sidebar-logo-text');
|
||||||
|
const sidebarTexts = document.querySelectorAll('.sidebar-text');
|
||||||
|
|
||||||
|
toggleButton.addEventListener('click', () => {
|
||||||
|
const isCollapsed = sidebar.classList.contains('w-20');
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
sidebar.classList.remove('w-20');
|
||||||
|
sidebar.classList.add('w-64');
|
||||||
|
setTimeout(() => {
|
||||||
|
logoText.classList.remove('opacity-0');
|
||||||
|
sidebarTexts.forEach(text => text.classList.remove('opacity-0'));
|
||||||
|
}, 150);
|
||||||
|
} else {
|
||||||
|
logoText.classList.add('opacity-0');
|
||||||
|
sidebarTexts.forEach(text => text.classList.add('opacity-0'));
|
||||||
|
sidebar.classList.remove('w-64');
|
||||||
|
sidebar.classList.add('w-20');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AdminSidebar } from "@/components/admin/AdminSidebar";
|
import { AdminShell } from "@/components/admin/AdminShell";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { authOptions } from "@/lib/auth";
|
import { authOptions } from "@/lib/auth";
|
||||||
|
|
||||||
@@ -8,12 +8,8 @@ export default async function AdminLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
return (
|
if (!session) {
|
||||||
<div className="flex min-h-screen bg-gray-50">
|
return <div className="min-h-screen bg-background">{children}</div>;
|
||||||
{session && <AdminSidebar />}
|
}
|
||||||
<main className="flex-1 px-8 py-8 overflow-y-auto">
|
return <AdminShell>{children}</AdminShell>;
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,12 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getActivityLog, getUpcomingReminders, getTranscripts } from "@/lib/lead-service";
|
|
||||||
import {
|
|
||||||
getLeadByIdWithTags,
|
|
||||||
getLeadFieldOptions,
|
|
||||||
getClientIdFromLead,
|
|
||||||
} from "@/lib/admin-queries";
|
|
||||||
import { LeadDetail } from "@/components/admin/leads/LeadDetail";
|
|
||||||
|
|
||||||
export default async function LeadDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
// Legacy route — Lead was renamed to Pipeline. Kept as a redirect stub so
|
||||||
|
// existing links/bookmarks to /admin/leads/<id> don't break.
|
||||||
|
export default async function LeadDetailRedirectPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
redirect(`/admin/pipeline/${id}`);
|
||||||
const [lead, options, convertedClientId] = await Promise.all([
|
|
||||||
getLeadByIdWithTags(id),
|
|
||||||
getLeadFieldOptions(),
|
|
||||||
getClientIdFromLead(id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!lead) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const [activities, reminders, transcripts] = await Promise.all([
|
|
||||||
getActivityLog(id),
|
|
||||||
getUpcomingReminders(id),
|
|
||||||
getTranscripts(id),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LeadDetail
|
|
||||||
lead={lead}
|
|
||||||
activities={activities}
|
|
||||||
reminders={reminders}
|
|
||||||
tags={lead.tags}
|
|
||||||
tagOptions={options.tags}
|
|
||||||
transcripts={transcripts}
|
|
||||||
convertedClientId={convertedClientId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,7 @@
|
|||||||
import { getLeadsWithTags, getLeadFieldOptions } from "@/lib/admin-queries";
|
import { redirect } from "next/navigation";
|
||||||
import { LeadsViewToggle } from "@/components/admin/leads/LeadsViewToggle";
|
|
||||||
import { CreateLeadModal } from "@/components/admin/leads/LeadForm";
|
|
||||||
import { PageHeader } from "@/components/admin/PageHeader";
|
|
||||||
|
|
||||||
export const revalidate = 0;
|
// Legacy route — Lead was renamed to Pipeline. Kept as a redirect stub so
|
||||||
|
// existing links/bookmarks to /admin/leads don't break.
|
||||||
export default async function LeadsPage() {
|
export default function LeadsRedirectPage() {
|
||||||
const [leads, options] = await Promise.all([
|
redirect("/admin/pipeline");
|
||||||
getLeadsWithTags(),
|
|
||||||
getLeadFieldOptions(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<PageHeader title="Lead Pipeline" action={<CreateLeadModal />} />
|
|
||||||
<LeadsViewToggle leads={leads} options={options} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { getActivityLog, getUpcomingReminders, getTranscripts } from "@/lib/lead-service";
|
||||||
|
import {
|
||||||
|
getLeadByIdWithTags,
|
||||||
|
getLeadFieldOptions,
|
||||||
|
getClientIdFromLead,
|
||||||
|
} from "@/lib/admin-queries";
|
||||||
|
import { LeadDetail } from "@/components/admin/leads/LeadDetail";
|
||||||
|
|
||||||
|
export default async function LeadDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
const [lead, options, convertedClientId] = await Promise.all([
|
||||||
|
getLeadByIdWithTags(id),
|
||||||
|
getLeadFieldOptions(),
|
||||||
|
getClientIdFromLead(id),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!lead) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [activities, reminders, transcripts] = await Promise.all([
|
||||||
|
getActivityLog(id),
|
||||||
|
getUpcomingReminders(id),
|
||||||
|
getTranscripts(id),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LeadDetail
|
||||||
|
lead={lead}
|
||||||
|
activities={activities}
|
||||||
|
reminders={reminders}
|
||||||
|
tags={lead.tags}
|
||||||
|
tagOptions={options.tags}
|
||||||
|
transcripts={transcripts}
|
||||||
|
convertedClientId={convertedClientId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@ export async function convertLeadToClient(leadId: string): Promise<void> {
|
|||||||
// Archive the lead but keep its "won" status (traceability without clutter).
|
// Archive the lead but keep its "won" status (traceability without clutter).
|
||||||
await db.update(leads).set({ archived: true }).where(eq(leads.id, leadId));
|
await db.update(leads).set({ archived: true }).where(eq(leads.id, leadId));
|
||||||
|
|
||||||
revalidatePath("/admin/leads");
|
revalidatePath("/admin/pipeline");
|
||||||
revalidatePath("/admin");
|
revalidatePath("/admin");
|
||||||
redirect(`/admin/clients/${clientId}`);
|
redirect(`/admin/clients/${clientId}`);
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ export async function createLead(data: z.infer<typeof createLeadSchema>) {
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
revalidatePath("/admin/leads");
|
revalidatePath("/admin/pipeline");
|
||||||
return { success: true, lead };
|
return { success: true, lead };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("createLead error:", error);
|
console.error("createLead error:", error);
|
||||||
@@ -118,8 +118,8 @@ export async function updateLead(id: string, data: z.infer<typeof updateLeadSche
|
|||||||
.where(eq(leads.id, id))
|
.where(eq(leads.id, id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
revalidatePath("/admin/leads");
|
revalidatePath("/admin/pipeline");
|
||||||
revalidatePath(`/admin/leads/${id}`);
|
revalidatePath(`/admin/pipeline/${id}`);
|
||||||
return { success: true, lead: updated };
|
return { success: true, lead: updated };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("updateLead error:", error);
|
console.error("updateLead error:", error);
|
||||||
@@ -130,7 +130,7 @@ export async function updateLead(id: string, data: z.infer<typeof updateLeadSche
|
|||||||
export async function deleteLead(id: string) {
|
export async function deleteLead(id: string) {
|
||||||
try {
|
try {
|
||||||
await db.delete(leads).where(eq(leads.id, id));
|
await db.delete(leads).where(eq(leads.id, id));
|
||||||
revalidatePath("/admin/leads");
|
revalidatePath("/admin/pipeline");
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("deleteLead error:", error);
|
console.error("deleteLead error:", error);
|
||||||
@@ -155,7 +155,7 @@ export async function logActivity(data: z.infer<typeof createActivitySchema>) {
|
|||||||
notes: parsed.data.notes,
|
notes: parsed.data.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidatePath(`/admin/leads/${parsed.data.lead_id}`);
|
revalidatePath(`/admin/pipeline/${parsed.data.lead_id}`);
|
||||||
return { success: true, activity };
|
return { success: true, activity };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("logActivity error:", error);
|
console.error("logActivity error:", error);
|
||||||
@@ -205,7 +205,7 @@ export async function assignQuoteToLead(data: z.infer<typeof assignQuoteSchema>)
|
|||||||
// Update lead status to "proposal_sent"
|
// Update lead status to "proposal_sent"
|
||||||
await updateLeadStage(leadId, "proposal_sent");
|
await updateLeadStage(leadId, "proposal_sent");
|
||||||
|
|
||||||
revalidatePath(`/admin/leads/${leadId}`);
|
revalidatePath(`/admin/pipeline/${leadId}`);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("assignQuoteToLead error:", error);
|
console.error("assignQuoteToLead error:", error);
|
||||||
@@ -252,8 +252,8 @@ export async function updateLeadField(
|
|||||||
.where(eq(leads.id, leadId));
|
.where(eq(leads.id, leadId));
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidatePath("/admin/leads");
|
revalidatePath("/admin/pipeline");
|
||||||
revalidatePath(`/admin/leads/${leadId}`);
|
revalidatePath(`/admin/pipeline/${leadId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Lead tags (Phase 14, CRM-09) — polymorphic `tags` table, entity_type="leads" ─
|
// ── Lead tags (Phase 14, CRM-09) — polymorphic `tags` table, entity_type="leads" ─
|
||||||
@@ -270,8 +270,8 @@ export async function addLeadTag(leadId: string, value: string) {
|
|||||||
.values({ entity_type: LEADS_TAG_ENTITY, entity_id: leadId, name: trimmed })
|
.values({ entity_type: LEADS_TAG_ENTITY, entity_id: leadId, name: trimmed })
|
||||||
.onConflictDoNothing();
|
.onConflictDoNothing();
|
||||||
|
|
||||||
revalidatePath("/admin/leads");
|
revalidatePath("/admin/pipeline");
|
||||||
revalidatePath(`/admin/leads/${leadId}`);
|
revalidatePath(`/admin/pipeline/${leadId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeLeadTag(leadId: string, value: string) {
|
export async function removeLeadTag(leadId: string, value: string) {
|
||||||
@@ -287,8 +287,8 @@ export async function removeLeadTag(leadId: string, value: string) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
revalidatePath("/admin/leads");
|
revalidatePath("/admin/pipeline");
|
||||||
revalidatePath(`/admin/leads/${leadId}`);
|
revalidatePath(`/admin/pipeline/${leadId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renameLeadTag(oldValue: string, newValue: string) {
|
export async function renameLeadTag(oldValue: string, newValue: string) {
|
||||||
@@ -302,7 +302,7 @@ export async function renameLeadTag(oldValue: string, newValue: string) {
|
|||||||
.set({ name: next })
|
.set({ name: next })
|
||||||
.where(and(eq(tags.entity_type, LEADS_TAG_ENTITY), eq(tags.name, oldValue)));
|
.where(and(eq(tags.entity_type, LEADS_TAG_ENTITY), eq(tags.name, oldValue)));
|
||||||
|
|
||||||
revalidatePath("/admin/leads");
|
revalidatePath("/admin/pipeline");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Transcript actions (Phase 20 — KB-02) ────────────────────────────────────
|
// ── Transcript actions (Phase 20 — KB-02) ────────────────────────────────────
|
||||||
@@ -334,7 +334,7 @@ export async function addTranscript(data: z.infer<typeof addTranscriptSchema>) {
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
revalidatePath(`/admin/leads/${parsed.data.lead_id}`);
|
revalidatePath(`/admin/pipeline/${parsed.data.lead_id}`);
|
||||||
return { success: true, transcript };
|
return { success: true, transcript };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("addTranscript error:", error);
|
console.error("addTranscript error:", error);
|
||||||
@@ -352,7 +352,7 @@ export async function deleteTranscript(transcriptId: string, leadId: string) {
|
|||||||
.delete(clientTranscripts)
|
.delete(clientTranscripts)
|
||||||
.where(eq(clientTranscripts.id, transcriptId));
|
.where(eq(clientTranscripts.id, transcriptId));
|
||||||
|
|
||||||
revalidatePath(`/admin/leads/${leadId}`);
|
revalidatePath(`/admin/pipeline/${leadId}`);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("deleteTranscript error:", error);
|
console.error("deleteTranscript error:", error);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { getLeadsWithTags, getLeadFieldOptions } from "@/lib/admin-queries";
|
||||||
|
import { LeadsViewToggle } from "@/components/admin/leads/LeadsViewToggle";
|
||||||
|
import { CreateLeadModal } from "@/components/admin/leads/LeadForm";
|
||||||
|
import { PageHeader } from "@/components/admin/PageHeader";
|
||||||
|
|
||||||
|
export const revalidate = 0;
|
||||||
|
|
||||||
|
export default async function LeadsPage() {
|
||||||
|
const [leads, options] = await Promise.all([
|
||||||
|
getLeadsWithTags(),
|
||||||
|
getLeadFieldOptions(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<PageHeader
|
||||||
|
title="Lead Pipeline"
|
||||||
|
subtitle="Gestisci e monitora i tuoi contatti commerciali"
|
||||||
|
action={<CreateLeadModal />}
|
||||||
|
/>
|
||||||
|
<LeadsViewToggle leads={leads} options={options} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+11
-1
@@ -109,7 +109,7 @@
|
|||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
/* Font */
|
/* Font */
|
||||||
--font-sans: var(--font-geist-sans), system-ui, -apple-system, BlinkMacSystemFont,
|
--font-sans: var(--font-plus-jakarta-sans), system-ui, -apple-system, BlinkMacSystemFont,
|
||||||
"Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
"Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
|
|
||||||
@@ -149,6 +149,16 @@
|
|||||||
--color-tertiary: var(--tertiary);
|
--color-tertiary: var(--tertiary);
|
||||||
--color-bg-subtle: var(--bg-subtle);
|
--color-bg-subtle: var(--bg-subtle);
|
||||||
--color-border-light: var(--border-light);
|
--color-border-light: var(--border-light);
|
||||||
|
|
||||||
|
/* Radius — wire the base --radius into Tailwind's radius scale so
|
||||||
|
rounded-lg / rounded-xl resolve off a single token. */
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 0.25rem);
|
||||||
|
|
||||||
|
/* Luxury shadows — very low-opacity, wide-spread card elevation
|
||||||
|
("Quiet Luxury" design system). */
|
||||||
|
--shadow-card: 0 4px 20px rgba(0, 0, 0, 0.01);
|
||||||
|
--shadow-card-hover: 0 8px 30px rgba(0, 0, 0, 0.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
|
|||||||
+5
-4
@@ -1,10 +1,11 @@
|
|||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Plus_Jakarta_Sans, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const plusJakartaSans = Plus_Jakarta_Sans({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-plus-jakarta-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
weight: ["300", "400", "500", "600", "700"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const geistMono = Geist_Mono({
|
||||||
@@ -30,7 +31,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="it"
|
lang="it"
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
className={`${plusJakartaSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||||
>
|
>
|
||||||
<body className="min-h-full flex flex-col bg-background text-foreground">
|
<body className="min-h-full flex flex-col bg-background text-foreground">
|
||||||
<script
|
<script
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Menu, User } from "lucide-react";
|
||||||
|
import { AdminSidebar } from "@/components/admin/AdminSidebar";
|
||||||
|
|
||||||
|
const SIDEBAR_STORAGE_KEY = "iamcavalli:sidebar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client shell for every /admin/* page: collapsible sidebar + top header +
|
||||||
|
* <main>. Collapse state is persisted to localStorage and read on mount
|
||||||
|
* (starts expanded on server/first paint to avoid a hydration mismatch,
|
||||||
|
* then reconciles client-side — mirrors the pattern used by useTheme).
|
||||||
|
*/
|
||||||
|
export function AdminShell({ children }: { children: React.ReactNode }) {
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = window.localStorage.getItem(SIDEBAR_STORAGE_KEY);
|
||||||
|
if (stored === "collapsed") setCollapsed(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function toggleCollapsed() {
|
||||||
|
setCollapsed((prev) => {
|
||||||
|
const next = !prev;
|
||||||
|
window.localStorage.setItem(SIDEBAR_STORAGE_KEY, next ? "collapsed" : "expanded");
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-background">
|
||||||
|
<AdminSidebar collapsed={collapsed} />
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col min-w-0">
|
||||||
|
{/* Top header bar */}
|
||||||
|
<header className="h-16 shrink-0 bg-card border-b border-border px-6 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleCollapsed}
|
||||||
|
title="Espandi/Comprimi Sidebar"
|
||||||
|
aria-label="Espandi/Comprimi Sidebar"
|
||||||
|
className="p-2 -ml-2 rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground transition-colors focus:outline-none"
|
||||||
|
>
|
||||||
|
<Menu className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">Area di Lavoro</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-xs font-semibold text-foreground">Admin</span>
|
||||||
|
{/* TODO: gestione utenti/collaboratori — placeholder avatar until
|
||||||
|
real user accounts + uploaded profile images are wired up. */}
|
||||||
|
<div className="w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center shrink-0">
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="flex-1 px-8 py-8 overflow-y-auto">{children}</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,10 +15,11 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import ThemeToggle from "@/components/ThemeToggle";
|
import ThemeToggle from "@/components/ThemeToggle";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
{ href: "/admin", label: "Dashboard", icon: LayoutDashboard, exact: true },
|
{ href: "/admin", label: "Dashboard", icon: LayoutDashboard, exact: true },
|
||||||
{ href: "/admin/leads", label: "Lead", icon: Zap },
|
{ href: "/admin/pipeline", label: "Pipeline", icon: Zap },
|
||||||
{ href: "/admin/clients", label: "Clienti", icon: Users },
|
{ href: "/admin/clients", label: "Clienti", icon: Users },
|
||||||
{ href: "/admin/projects", label: "Progetti", icon: FolderOpen },
|
{ href: "/admin/projects", label: "Progetti", icon: FolderOpen },
|
||||||
{ href: "/admin/preventivi", label: "Preventivi", icon: FileText },
|
{ href: "/admin/preventivi", label: "Preventivi", icon: FileText },
|
||||||
@@ -27,17 +28,32 @@ const NAV_ITEMS = [
|
|||||||
{ href: "/admin/impostazioni", label: "Impostazioni", icon: Settings },
|
{ href: "/admin/impostazioni", label: "Impostazioni", icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function AdminSidebar() {
|
export function AdminSidebar({ collapsed = false }: { collapsed?: boolean }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const isActive = (href: string, exact?: boolean) =>
|
const isActive = (href: string, exact?: boolean) =>
|
||||||
exact ? pathname === href : pathname === href || pathname.startsWith(href + "/");
|
exact ? pathname === href : pathname === href || pathname.startsWith(href + "/");
|
||||||
|
|
||||||
|
// Text fades out immediately on collapse, but only fades back in once the
|
||||||
|
// width transition is underway on expand (avoids reflow mid-animation).
|
||||||
|
const textClass = cn(
|
||||||
|
"whitespace-nowrap transition-opacity duration-200 delay-150",
|
||||||
|
collapsed && "opacity-0 pointer-events-none delay-0"
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-56 shrink-0 min-h-screen bg-[#1A463C] flex flex-col">
|
<aside
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 min-h-screen bg-[#1A463C] flex flex-col overflow-hidden",
|
||||||
|
"transition-[width] duration-350 ease-[cubic-bezier(0.4,0,0.2,1)]",
|
||||||
|
collapsed ? "w-20" : "w-64"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="px-5 py-5 border-b border-white/10">
|
<div className="px-5 py-5 border-b border-white/10">
|
||||||
<span className="font-bold text-white tracking-tight text-sm">iamcavalli</span>
|
<span className={cn("font-bold text-white tracking-tight text-sm", textClass)}>
|
||||||
|
iamcavalli
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nav links */}
|
{/* Nav links */}
|
||||||
@@ -48,14 +64,16 @@ export function AdminSidebar() {
|
|||||||
<Link
|
<Link
|
||||||
key={href}
|
key={href}
|
||||||
href={href}
|
href={href}
|
||||||
className={`flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors ${
|
title={collapsed ? label : undefined}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors",
|
||||||
active
|
active
|
||||||
? "bg-white/15 text-white font-medium"
|
? "bg-white/15 text-white font-medium"
|
||||||
: "text-white/65 hover:text-white hover:bg-white/10"
|
: "text-white/65 hover:text-white hover:bg-white/10"
|
||||||
}`}
|
)}
|
||||||
>
|
>
|
||||||
<Icon size={16} strokeWidth={1.8} />
|
<Icon size={16} strokeWidth={1.8} className="shrink-0" />
|
||||||
{label}
|
<span className={textClass}>{label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -64,15 +82,16 @@ export function AdminSidebar() {
|
|||||||
{/* Theme + Logout */}
|
{/* Theme + Logout */}
|
||||||
<div className="px-3 py-4 border-t border-white/10 flex flex-col gap-2">
|
<div className="px-3 py-4 border-t border-white/10 flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between px-3">
|
<div className="flex items-center justify-between px-3">
|
||||||
<span className="text-xs text-white/50">Tema</span>
|
<span className={cn("text-xs text-white/50", textClass)}>Tema</span>
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => signOut({ callbackUrl: "/admin/login" })}
|
onClick={() => signOut({ callbackUrl: "/admin/login" })}
|
||||||
|
title={collapsed ? "Esci" : undefined}
|
||||||
className="flex items-center gap-3 px-3 py-2 w-full rounded-md text-sm text-white/65 hover:text-white hover:bg-white/10 transition-colors"
|
className="flex items-center gap-3 px-3 py-2 w-full rounded-md text-sm text-white/65 hover:text-white hover:bg-white/10 transition-colors"
|
||||||
>
|
>
|
||||||
<LogOut size={16} strokeWidth={1.8} />
|
<LogOut size={16} strokeWidth={1.8} className="shrink-0" />
|
||||||
Esci
|
<span className={textClass}>Esci</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ export function PageHeader({ title, subtitle, action }: {
|
|||||||
action?: ReactNode;
|
action?: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 flex items-start justify-between gap-4">
|
<div className="mb-6 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-[#1a1a1a]">{title}</h1>
|
<h1 className="text-2xl font-semibold tracking-tight text-foreground">{title}</h1>
|
||||||
{subtitle && <p className="mt-1 text-sm text-[#71717a]">{subtitle}</p>}
|
{subtitle && <p className="mt-1 text-xs text-muted-foreground">{subtitle}</p>}
|
||||||
</div>
|
</div>
|
||||||
{action}
|
{action}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export async function FollowUpWidget() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/leads"
|
href="/admin/pipeline"
|
||||||
className="text-xs font-medium text-[#71717a] hover:text-[#1A463C] transition-colors"
|
className="text-xs font-medium text-[#71717a] hover:text-[#1A463C] transition-colors"
|
||||||
>
|
>
|
||||||
Vedi tutti →
|
Vedi tutti →
|
||||||
@@ -59,7 +59,7 @@ export async function FollowUpWidget() {
|
|||||||
{visible.map((lead) => (
|
{visible.map((lead) => (
|
||||||
<li key={lead.id}>
|
<li key={lead.id}>
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/leads/${lead.id}`}
|
href={`/admin/pipeline/${lead.id}`}
|
||||||
className="flex items-center gap-3 px-5 py-3 hover:bg-[#f9f9f9] transition-colors group"
|
className="flex items-center gap-3 px-5 py-3 hover:bg-[#f9f9f9] transition-colors group"
|
||||||
>
|
>
|
||||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#1A463C]/10 text-[#1A463C] text-xs font-bold">
|
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#1A463C]/10 text-[#1A463C] text-xs font-bold">
|
||||||
@@ -85,7 +85,7 @@ export async function FollowUpWidget() {
|
|||||||
|
|
||||||
{leads.length > MAX_ROWS && (
|
{leads.length > MAX_ROWS && (
|
||||||
<Link
|
<Link
|
||||||
href="/admin/leads"
|
href="/admin/pipeline"
|
||||||
className="block px-5 py-2.5 text-center text-xs font-medium text-[#71717a] hover:text-[#1A463C] border-t border-[#f4f4f5] transition-colors"
|
className="block px-5 py-2.5 text-center text-xs font-medium text-[#71717a] hover:text-[#1A463C] border-t border-[#f4f4f5] transition-colors"
|
||||||
>
|
>
|
||||||
Vedi tutti i {leads.length} lead
|
Vedi tutti i {leads.length} lead
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
renameLeadTag,
|
renameLeadTag,
|
||||||
deleteTranscript,
|
deleteTranscript,
|
||||||
convertLeadToClient,
|
convertLeadToClient,
|
||||||
} from "@/app/admin/leads/actions";
|
} from "@/app/admin/pipeline/actions";
|
||||||
import { formatDistanceToNow, format } from "date-fns";
|
import { formatDistanceToNow, format } from "date-fns";
|
||||||
import { it } from "date-fns/locale";
|
import { it } from "date-fns/locale";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { createLeadSchema, LEAD_STAGES, type CreateLeadInput } from "@/lib/lead-validators";
|
import { createLeadSchema, LEAD_STAGES, type CreateLeadInput } from "@/lib/lead-validators";
|
||||||
import { Lead } from "@/db/schema";
|
import { Lead } from "@/db/schema";
|
||||||
import { createLead, updateLead } from "@/app/admin/leads/actions";
|
import { createLead, updateLead } from "@/app/admin/pipeline/actions";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|||||||
@@ -3,28 +3,17 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState, useRef, useEffect, useTransition } from "react";
|
import { useState, useRef, useEffect, useTransition } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Check } from "lucide-react";
|
import { Check, ArrowRight } from "lucide-react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { EditableCell } from "@/components/ui/editable-cell";
|
import { EditableCell } from "@/components/ui/editable-cell";
|
||||||
import { OptionMultiSelect } from "@/components/ui/option-multi-select";
|
import { OptionMultiSelect } from "@/components/ui/option-multi-select";
|
||||||
|
import { StatusBadge } from "@/components/ui/StatusBadge";
|
||||||
import {
|
import {
|
||||||
updateLeadField,
|
updateLeadField,
|
||||||
addLeadTag,
|
addLeadTag,
|
||||||
removeLeadTag,
|
removeLeadTag,
|
||||||
renameLeadTag,
|
renameLeadTag,
|
||||||
} from "@/app/admin/leads/actions";
|
} from "@/app/admin/pipeline/actions";
|
||||||
import type { LeadWithTags, LeadFieldOptions } from "@/lib/admin-queries";
|
import type { LeadWithTags, LeadFieldOptions } from "@/lib/admin-queries";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const STAGE_COLOR: Record<string, string> = {
|
|
||||||
contacted: "bg-blue-100 text-blue-800",
|
|
||||||
qualified: "bg-purple-100 text-purple-800",
|
|
||||||
proposal_sent: "bg-amber-100 text-amber-800",
|
|
||||||
negotiating: "bg-orange-100 text-orange-800",
|
|
||||||
won: "bg-green-100 text-green-800",
|
|
||||||
lost: "bg-red-100 text-red-800",
|
|
||||||
};
|
|
||||||
|
|
||||||
const COLUMN_HEADERS = [
|
const COLUMN_HEADERS = [
|
||||||
"Nome",
|
"Nome",
|
||||||
@@ -61,23 +50,15 @@ function StatusCell({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="relative w-full min-w-[120px]">
|
<div ref={containerRef} className="relative w-full min-w-[120px] flex justify-center">
|
||||||
<div
|
<div
|
||||||
onClick={() => setIsOpen((v) => !v)}
|
onClick={() => setIsOpen((v) => !v)}
|
||||||
className="flex items-center gap-1 px-2 py-1 rounded transition-colors duration-150 min-h-[28px] cursor-pointer hover:bg-[#f0f0f0]"
|
className="flex items-center gap-1 px-2 py-1 rounded transition-colors duration-150 min-h-[28px] cursor-pointer hover:bg-muted"
|
||||||
>
|
>
|
||||||
<Badge
|
<StatusBadge status={lead.status} />
|
||||||
variant="outline"
|
|
||||||
className={cn(
|
|
||||||
STAGE_COLOR[lead.status] ?? "",
|
|
||||||
"border-transparent text-xs px-2 py-0.5 font-medium truncate"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{lead.status.replace(/_/g, " ")}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="absolute top-full left-0 mt-1 bg-white border border-[#e5e7eb] rounded-md shadow-md p-1.5 z-20 min-w-[180px]">
|
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-1 bg-popover border border-border rounded-md shadow-card-hover p-1.5 z-20 min-w-[180px]">
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
{options.status.map((stage) => (
|
{options.status.map((stage) => (
|
||||||
<button
|
<button
|
||||||
@@ -87,17 +68,12 @@ function StatusCell({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
run(() => updateLeadField(lead.id, "status", stage));
|
run(() => updateLeadField(lead.id, "status", stage));
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2 rounded px-1.5 py-1 hover:bg-[#f5f5f5] text-left"
|
className="flex items-center gap-2 rounded px-1.5 py-1 hover:bg-muted text-left"
|
||||||
>
|
>
|
||||||
<span className="w-3.5 flex-shrink-0">
|
<span className="w-3.5 flex-shrink-0">
|
||||||
{lead.status === stage && <Check className="h-3.5 w-3.5 text-[#1A463C]" />}
|
{lead.status === stage && <Check className="h-3.5 w-3.5 text-primary" />}
|
||||||
</span>
|
</span>
|
||||||
<Badge
|
<StatusBadge status={stage} />
|
||||||
variant="outline"
|
|
||||||
className={cn(STAGE_COLOR[stage] ?? "", "border-transparent text-xs px-2 py-0.5 font-medium truncate")}
|
|
||||||
>
|
|
||||||
{stage.replace(/_/g, " ")}
|
|
||||||
</Badge>
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -132,8 +108,8 @@ function LeadRow({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr className="border-b border-[#e5e7eb] hover:bg-[#f9f9f9] transition-colors duration-150">
|
<tr className="hover:bg-muted/40 transition-colors duration-150">
|
||||||
<td className="py-2 px-3 font-medium text-[#1a1a1a] min-w-[160px]">
|
<td className="py-4 px-6 font-medium text-foreground min-w-[160px]">
|
||||||
<EditableCell
|
<EditableCell
|
||||||
value={lead.name}
|
value={lead.name}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -141,7 +117,7 @@ function LeadRow({
|
|||||||
onSave={(v) => run(() => updateLeadField(lead.id, "name", v))}
|
onSave={(v) => run(() => updateLeadField(lead.id, "name", v))}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3 min-w-[160px]">
|
<td className="py-4 px-6 min-w-[160px] text-muted-foreground text-xs">
|
||||||
<EditableCell
|
<EditableCell
|
||||||
value={lead.email ?? ""}
|
value={lead.email ?? ""}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -149,7 +125,7 @@ function LeadRow({
|
|||||||
onSave={(v) => run(() => updateLeadField(lead.id, "email", v))}
|
onSave={(v) => run(() => updateLeadField(lead.id, "email", v))}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3 min-w-[140px]">
|
<td className="py-4 px-6 min-w-[140px] text-muted-foreground text-xs font-mono">
|
||||||
<EditableCell
|
<EditableCell
|
||||||
value={lead.phone ?? ""}
|
value={lead.phone ?? ""}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -157,7 +133,7 @@ function LeadRow({
|
|||||||
onSave={(v) => run(() => updateLeadField(lead.id, "phone", v))}
|
onSave={(v) => run(() => updateLeadField(lead.id, "phone", v))}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3 min-w-[140px]">
|
<td className="py-4 px-6 min-w-[140px]">
|
||||||
<EditableCell
|
<EditableCell
|
||||||
value={lead.company ?? ""}
|
value={lead.company ?? ""}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -165,10 +141,10 @@ function LeadRow({
|
|||||||
onSave={(v) => run(() => updateLeadField(lead.id, "company", v))}
|
onSave={(v) => run(() => updateLeadField(lead.id, "company", v))}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3 min-w-[120px]">
|
<td className="py-4 px-6 min-w-[120px] text-center">
|
||||||
<StatusCell lead={lead} options={options} run={run} />
|
<StatusCell lead={lead} options={options} run={run} />
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3 min-w-[180px]">
|
<td className="py-4 px-6 min-w-[180px]">
|
||||||
<EditableCell
|
<EditableCell
|
||||||
value={lead.next_action ?? ""}
|
value={lead.next_action ?? ""}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -176,7 +152,7 @@ function LeadRow({
|
|||||||
onSave={(v) => run(() => updateLeadField(lead.id, "next_action", v))}
|
onSave={(v) => run(() => updateLeadField(lead.id, "next_action", v))}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3 min-w-[180px]">
|
<td className="py-4 px-6 min-w-[180px] text-center">
|
||||||
<OptionMultiSelect
|
<OptionMultiSelect
|
||||||
values={lead.tags}
|
values={lead.tags}
|
||||||
options={options.tags}
|
options={options.tags}
|
||||||
@@ -185,15 +161,19 @@ function LeadRow({
|
|||||||
onRename={(o, n) => run(() => renameLeadTag(o, n))}
|
onRename={(o, n) => run(() => renameLeadTag(o, n))}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-3 min-w-[80px]">
|
<td className="py-4 px-6 min-w-[80px] text-right">
|
||||||
<Button variant="ghost" size="sm" asChild>
|
<Link
|
||||||
<Link href={`/admin/leads/${lead.id}`}>Dettagli</Link>
|
href={`/admin/pipeline/${lead.id}`}
|
||||||
</Button>
|
className="text-xs font-medium text-primary hover:text-primary/80 inline-flex items-center gap-1 group transition-colors"
|
||||||
|
>
|
||||||
|
Dettagli
|
||||||
|
<ArrowRight className="w-3 h-3 transform group-hover:translate-x-0.5 transition-transform" />
|
||||||
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{error && (
|
{error && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={COL_COUNT} className="py-1 px-3 text-xs text-red-600">
|
<td colSpan={COL_COUNT} className="py-1 px-6 text-xs text-destructive">
|
||||||
{error}
|
{error}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -210,22 +190,19 @@ export function LeadTable({
|
|||||||
options: LeadFieldOptions;
|
options: LeadFieldOptions;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl border border-[#e5e7eb]">
|
<div className="bg-card rounded-xl border border-border shadow-card overflow-hidden">
|
||||||
<div className="overflow-x-auto min-h-[300px]">
|
<div className="overflow-x-auto min-h-[300px]">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-left border-collapse">
|
||||||
<thead className="bg-[#f9f9f9] border-b border-[#e5e7eb] sticky top-0 z-[1]">
|
<thead>
|
||||||
<tr>
|
<tr className="border-b border-border bg-muted/50 text-muted-foreground text-[11px] font-semibold uppercase tracking-wider">
|
||||||
{COLUMN_HEADERS.map((header) => (
|
{COLUMN_HEADERS.map((header) => (
|
||||||
<th
|
<th key={header} className="py-4 px-6">
|
||||||
key={header}
|
|
||||||
className="text-left py-2 px-3 font-semibold text-[#71717a]"
|
|
||||||
>
|
|
||||||
{header}
|
{header}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody className="divide-y divide-border text-sm">
|
||||||
{leads.map((lead) => (
|
{leads.map((lead) => (
|
||||||
<LeadRow key={lead.id} lead={lead} options={options} />
|
<LeadRow key={lead.id} lead={lead} options={options} />
|
||||||
))}
|
))}
|
||||||
@@ -234,7 +211,7 @@ export function LeadTable({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{leads.length === 0 && (
|
{leads.length === 0 && (
|
||||||
<div className="text-center py-8 text-gray-500">Nessun lead trovato</div>
|
<div className="text-center py-8 text-muted-foreground">Nessun lead trovato</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
useDroppable,
|
useDroppable,
|
||||||
useDraggable,
|
useDraggable,
|
||||||
} from "@dnd-kit/core";
|
} from "@dnd-kit/core";
|
||||||
import { updateLeadField } from "@/app/admin/leads/actions";
|
import { updateLeadField } from "@/app/admin/pipeline/actions";
|
||||||
import type { LeadWithTags } from "@/lib/admin-queries";
|
import type { LeadWithTags } from "@/lib/admin-queries";
|
||||||
|
|
||||||
type LeadStage = "contacted" | "qualified" | "proposal_sent" | "negotiating" | "won" | "lost";
|
type LeadStage = "contacted" | "qualified" | "proposal_sent" | "negotiating" | "won" | "lost";
|
||||||
@@ -24,12 +24,12 @@ const LEAD_COLUMNS: {
|
|||||||
headerClass: string;
|
headerClass: string;
|
||||||
dotClass: string;
|
dotClass: string;
|
||||||
}[] = [
|
}[] = [
|
||||||
{ id: "contacted", label: "Contattato", headerClass: "text-[#71717a]", dotClass: "bg-[#d4d4d8]" },
|
{ id: "contacted", label: "Contattato", headerClass: "text-muted-foreground", dotClass: "bg-blue-400 dark:bg-blue-500" },
|
||||||
{ id: "qualified", label: "Qualificato", headerClass: "text-purple-700", dotClass: "bg-purple-400" },
|
{ id: "qualified", label: "Qualificato", headerClass: "text-purple-700 dark:text-purple-400", dotClass: "bg-purple-400 dark:bg-purple-500" },
|
||||||
{ id: "proposal_sent", label: "Offerta inviata", headerClass: "text-amber-700", dotClass: "bg-amber-400" },
|
{ id: "proposal_sent", label: "Offerta inviata", headerClass: "text-amber-700 dark:text-amber-400", dotClass: "bg-amber-400 dark:bg-amber-500" },
|
||||||
{ id: "negotiating", label: "Trattativa", headerClass: "text-orange-700", dotClass: "bg-orange-400" },
|
{ id: "negotiating", label: "Trattativa", headerClass: "text-orange-700 dark:text-orange-400", dotClass: "bg-orange-400 dark:bg-orange-500" },
|
||||||
{ id: "won", label: "Vinto", headerClass: "text-green-700", dotClass: "bg-green-500" },
|
{ id: "won", label: "Vinto", headerClass: "text-emerald-700 dark:text-emerald-400", dotClass: "bg-emerald-500" },
|
||||||
{ id: "lost", label: "Perso", headerClass: "text-red-700", dotClass: "bg-red-400" },
|
{ id: "lost", label: "Perso", headerClass: "text-red-700 dark:text-red-400", dotClass: "bg-red-400 dark:bg-red-500" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const VALID_STAGES = LEAD_COLUMNS.map((c) => c.id) as string[];
|
const VALID_STAGES = LEAD_COLUMNS.map((c) => c.id) as string[];
|
||||||
@@ -49,26 +49,26 @@ function DroppableColumn({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
className={`flex flex-col rounded-xl border-2 transition-colors ${
|
className={`flex flex-col rounded-xl border p-4 transition-colors ${
|
||||||
isOver ? "border-[#1A463C] bg-[#1A463C]/5" : "border-[#e5e7eb] bg-[#f9f9f9]"
|
isOver ? "border-primary bg-primary/5" : "border-border bg-muted/60"
|
||||||
} min-h-[240px]`}
|
} min-h-[240px]`}
|
||||||
>
|
>
|
||||||
<div className={`px-4 py-3 flex items-center justify-between ${headerClass}`}>
|
<div className={`flex items-center justify-between pb-2 mb-2 border-b border-border ${headerClass}`}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`w-2 h-2 rounded-full ${dotClass}`} />
|
<span className={`w-2 h-2 rounded-full ${dotClass}`} />
|
||||||
<span className="text-xs font-bold uppercase tracking-wider">{label}</span>
|
<span className="text-xs font-bold uppercase tracking-wider">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-semibold tabular-nums bg-white rounded-full px-2 py-0.5 border border-[#e5e7eb]">
|
<span className="text-xs font-semibold tabular-nums bg-card rounded-full px-2 py-0.5 border border-border text-muted-foreground">
|
||||||
{leads.length}
|
{leads.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 p-3 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
{leads.map((lead) => (
|
{leads.map((lead) => (
|
||||||
<DraggableLeadCard key={lead.id} lead={lead} isActive={activeId === lead.id} />
|
<DraggableLeadCard key={lead.id} lead={lead} isActive={activeId === lead.id} />
|
||||||
))}
|
))}
|
||||||
{leads.length === 0 && (
|
{leads.length === 0 && (
|
||||||
<p className="text-xs text-[#d4d4d8] italic text-center py-10 select-none">
|
<p className="text-xs text-muted-foreground italic text-center py-10 select-none">
|
||||||
Nessun lead
|
Nessun lead
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -90,16 +90,16 @@ function DraggableLeadCard({ lead, isActive }: { lead: LeadWithTags; isActive: b
|
|||||||
style={style}
|
style={style}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
className={`bg-white rounded-lg border border-[#e5e7eb] px-3 py-2.5 cursor-grab active:cursor-grabbing shadow-sm select-none transition-opacity ${
|
className={`bg-card rounded-lg border border-border px-3 py-2.5 cursor-grab active:cursor-grabbing shadow-sm select-none transition-all duration-200 ${
|
||||||
isDragging ? "opacity-30" : "hover:border-[#1A463C]/40 hover:shadow"
|
isDragging ? "opacity-30" : "hover:border-primary/30 hover:shadow-card-hover"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<p className="text-sm font-medium text-[#1a1a1a]">{lead.name}</p>
|
<p className="text-sm font-medium text-foreground">{lead.name}</p>
|
||||||
{lead.company && (
|
{lead.company && (
|
||||||
<p className="text-xs text-[#71717a]">{lead.company}</p>
|
<p className="text-xs text-muted-foreground">{lead.company}</p>
|
||||||
)}
|
)}
|
||||||
{lead.next_action && (
|
{lead.next_action && (
|
||||||
<p className="text-xs text-[#71717a] mt-1 line-clamp-1">{lead.next_action}</p>
|
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">{lead.next_action}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -170,10 +170,10 @@ export function LeadsKanbanBoard({ leads }: { leads: LeadWithTags[] }) {
|
|||||||
|
|
||||||
<DragOverlay dropAnimation={null}>
|
<DragOverlay dropAnimation={null}>
|
||||||
{activeLead && (
|
{activeLead && (
|
||||||
<div className="bg-white rounded-lg border-2 border-[#1A463C] px-3 py-2.5 shadow-xl rotate-1 pointer-events-none">
|
<div className="bg-card rounded-lg border-2 border-primary px-3 py-2.5 shadow-xl rotate-1 pointer-events-none">
|
||||||
<p className="text-sm font-medium text-[#1a1a1a]">{activeLead.name}</p>
|
<p className="text-sm font-medium text-foreground">{activeLead.name}</p>
|
||||||
{activeLead.company && (
|
{activeLead.company && (
|
||||||
<p className="text-xs text-[#71717a]">{activeLead.company}</p>
|
<p className="text-xs text-muted-foreground">{activeLead.company}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { SearchInput } from "@/components/ui/SearchInput";
|
||||||
import { Search } from "lucide-react";
|
import { SegmentedToggle } from "@/components/ui/SegmentedToggle";
|
||||||
import { LeadTable } from "@/components/admin/leads/LeadTable";
|
import { LeadTable } from "@/components/admin/leads/LeadTable";
|
||||||
import { LeadsKanbanBoard } from "@/components/admin/leads/LeadsKanbanBoard";
|
import { LeadsKanbanBoard } from "@/components/admin/leads/LeadsKanbanBoard";
|
||||||
import type { LeadWithTags, LeadFieldOptions } from "@/lib/admin-queries";
|
import type { LeadWithTags, LeadFieldOptions } from "@/lib/admin-queries";
|
||||||
@@ -32,39 +32,22 @@ export function LeadsViewToggle({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
||||||
<div className="relative max-w-sm flex-1">
|
<SearchInput
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#71717a]" />
|
placeholder="Cerca lead..."
|
||||||
<Input
|
value={query}
|
||||||
type="text"
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Cerca lead..."
|
containerClassName="w-full md:w-80"
|
||||||
value={query}
|
/>
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
<SegmentedToggle
|
||||||
className="pl-9 h-9"
|
options={[
|
||||||
/>
|
{ value: "list", label: "Lista" },
|
||||||
</div>
|
{ value: "kanban", label: "Kanban" },
|
||||||
<div className="flex items-center gap-1 bg-[#f4f4f5] rounded-lg p-1 w-fit flex-shrink-0">
|
]}
|
||||||
<button
|
value={view}
|
||||||
onClick={() => setView("list")}
|
onChange={setView}
|
||||||
className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
|
className="self-start md:self-auto flex-shrink-0"
|
||||||
view === "list"
|
/>
|
||||||
? "bg-white text-[#1A463C] shadow-sm"
|
|
||||||
: "text-[#71717a] hover:text-[#1a1a1a]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Lista
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setView("kanban")}
|
|
||||||
className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${
|
|
||||||
view === "kanban"
|
|
||||||
? "bg-white text-[#1A463C] shadow-sm"
|
|
||||||
: "text-[#71717a] hover:text-[#1a1a1a]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Kanban
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{view === "list" ? (
|
{view === "list" ? (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createActivitySchema, ACTIVITY_TYPES } from "@/lib/lead-validators";
|
import { createActivitySchema, ACTIVITY_TYPES } from "@/lib/lead-validators";
|
||||||
import { logActivity } from "@/app/admin/leads/actions";
|
import { logActivity } from "@/app/admin/pipeline/actions";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { assignQuoteToLead } from "@/app/admin/leads/actions";
|
import { assignQuoteToLead } from "@/app/admin/pipeline/actions";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { addTranscript } from "@/app/admin/leads/actions";
|
import { addTranscript } from "@/app/admin/pipeline/actions";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Search } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search-icon input, extracted from the pattern originally inlined in
|
||||||
|
* LeadsViewToggle. Token-based so it themes correctly in dark mode.
|
||||||
|
*/
|
||||||
|
export function SearchInput({
|
||||||
|
className,
|
||||||
|
containerClassName,
|
||||||
|
...props
|
||||||
|
}: React.InputHTMLAttributes<HTMLInputElement> & { containerClassName?: string }) {
|
||||||
|
return (
|
||||||
|
<div className={cn("relative", containerClassName)}>
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={cn(
|
||||||
|
"w-full h-9 pl-9 pr-3 rounded-lg border border-border bg-card text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary transition-all duration-200",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type SegmentedToggleOption<T extends string> = {
|
||||||
|
value: T;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic segmented control (e.g. Lista/Kanban). Active option gets
|
||||||
|
* bg-card + shadow-sm to read as a "raised" tab against the bg-muted track.
|
||||||
|
*/
|
||||||
|
export function SegmentedToggle<T extends string>({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
options: SegmentedToggleOption<T>[];
|
||||||
|
value: T;
|
||||||
|
onChange: (value: T) => void;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-center gap-1 bg-muted rounded-lg p-1 w-fit", className)}>
|
||||||
|
{options.map((option) => {
|
||||||
|
const active = option.value === value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(option.value)}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1.5 rounded-md text-xs font-semibold transition-all duration-200",
|
||||||
|
active
|
||||||
|
? "bg-card text-primary shadow-sm"
|
||||||
|
: "text-muted-foreground hover:text-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lead pipeline stages. Kept in sync with LEAD_STAGES
|
||||||
|
* (src/lib/lead-validators.ts) — all 6 stages stay, per the CRM Kanban.
|
||||||
|
*/
|
||||||
|
export type LeadStage =
|
||||||
|
| "contacted"
|
||||||
|
| "qualified"
|
||||||
|
| "proposal_sent"
|
||||||
|
| "negotiating"
|
||||||
|
| "won"
|
||||||
|
| "lost";
|
||||||
|
|
||||||
|
const STAGE_STYLES: Record<LeadStage, string> = {
|
||||||
|
contacted:
|
||||||
|
"bg-blue-50 text-blue-600 border-blue-100 dark:bg-blue-950/40 dark:text-blue-300 dark:border-blue-900",
|
||||||
|
qualified:
|
||||||
|
"bg-purple-50 text-purple-600 border-purple-100 dark:bg-purple-950/40 dark:text-purple-300 dark:border-purple-900",
|
||||||
|
proposal_sent:
|
||||||
|
"bg-amber-50 text-amber-600 border-amber-100 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-900",
|
||||||
|
negotiating:
|
||||||
|
"bg-orange-50 text-orange-600 border-orange-100 dark:bg-orange-950/40 dark:text-orange-300 dark:border-orange-900",
|
||||||
|
won: "bg-emerald-50 text-emerald-600 border-emerald-100 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-900",
|
||||||
|
lost: "bg-red-50 text-red-600 border-red-100 dark:bg-red-950/40 dark:text-red-300 dark:border-red-900",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounded-full status pill for lead stages. Replaces the old scattered
|
||||||
|
* STAGE_COLOR maps in LeadTable/LeadsKanbanBoard — one source of truth for
|
||||||
|
* stage → color, with explicit dark: variants for legibility.
|
||||||
|
*/
|
||||||
|
export function StatusBadge({
|
||||||
|
status,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
status: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const styles = STAGE_STYLES[status as LeadStage] ?? "bg-muted text-muted-foreground border-border";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-semibold tracking-wide uppercase border whitespace-nowrap",
|
||||||
|
styles,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{status.replace(/_/g, " ")}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -110,7 +110,7 @@ export function EditableCell({
|
|||||||
"px-2 py-1 rounded transition-colors duration-150 text-sm",
|
"px-2 py-1 rounded transition-colors duration-150 text-sm",
|
||||||
disabled
|
disabled
|
||||||
? "cursor-not-allowed opacity-50"
|
? "cursor-not-allowed opacity-50"
|
||||||
: "cursor-pointer hover:bg-[#f0f0f0]",
|
: "cursor-pointer hover:bg-muted",
|
||||||
type === "textarea" && "line-clamp-2 max-w-md"
|
type === "textarea" && "line-clamp-2 max-w-md"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -81,11 +81,11 @@ export function OptionMultiSelect({
|
|||||||
onClick={() => !disabled && setIsOpen((v) => !v)}
|
onClick={() => !disabled && setIsOpen((v) => !v)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-wrap gap-1 items-center px-2 py-1 rounded transition-colors duration-150 min-h-[28px]",
|
"flex flex-wrap gap-1 items-center px-2 py-1 rounded transition-colors duration-150 min-h-[28px]",
|
||||||
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer hover:bg-[#f0f0f0]"
|
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer hover:bg-muted"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{values.length === 0 ? (
|
{values.length === 0 ? (
|
||||||
<span className="text-xs text-[#71717a]">—</span>
|
<span className="text-xs text-muted-foreground">—</span>
|
||||||
) : (
|
) : (
|
||||||
values.map((v) => (
|
values.map((v) => (
|
||||||
<Badge
|
<Badge
|
||||||
@@ -111,11 +111,11 @@ export function OptionMultiSelect({
|
|||||||
</Badge>
|
</Badge>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
<Plus className="h-3.5 w-3.5 text-[#71717a]" />
|
<Plus className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="absolute top-full left-0 mt-1 bg-white border border-[#e5e7eb] rounded-md shadow-md p-1.5 z-20 min-w-[220px] max-h-[280px] overflow-y-auto">
|
<div className="absolute top-full left-0 mt-1 bg-popover border border-border rounded-md shadow-md p-1.5 z-20 min-w-[220px] max-h-[280px] overflow-y-auto">
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -160,7 +160,7 @@ export function OptionMultiSelect({
|
|||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
key={opt}
|
key={opt}
|
||||||
className="flex items-center justify-between gap-1 rounded px-1.5 py-1 hover:bg-[#f5f5f5] group"
|
className="flex items-center justify-between gap-1 rounded px-1.5 py-1 hover:bg-muted group"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -168,7 +168,7 @@ export function OptionMultiSelect({
|
|||||||
className="flex items-center gap-2 flex-1 min-w-0 text-left"
|
className="flex items-center gap-2 flex-1 min-w-0 text-left"
|
||||||
>
|
>
|
||||||
<span className="w-3.5 flex-shrink-0">
|
<span className="w-3.5 flex-shrink-0">
|
||||||
{selected.has(opt) && <Check className="h-3.5 w-3.5 text-[#1A463C]" />}
|
{selected.has(opt) && <Check className="h-3.5 w-3.5 text-primary" />}
|
||||||
</span>
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -187,7 +187,7 @@ export function OptionMultiSelect({
|
|||||||
setRenameValue(opt);
|
setRenameValue(opt);
|
||||||
setRenaming(opt);
|
setRenaming(opt);
|
||||||
}}
|
}}
|
||||||
className="opacity-0 group-hover:opacity-100 text-[#71717a] hover:text-[#1a1a1a] p-0.5"
|
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-foreground p-0.5"
|
||||||
aria-label={`Rinomina ${opt}`}
|
aria-label={`Rinomina ${opt}`}
|
||||||
>
|
>
|
||||||
<Pencil className="h-3 w-3" />
|
<Pencil className="h-3 w-3" />
|
||||||
@@ -201,15 +201,15 @@ export function OptionMultiSelect({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={createFromQuery}
|
onClick={createFromQuery}
|
||||||
className="flex items-center gap-2 rounded px-1.5 py-1.5 hover:bg-[#f5f5f5] text-left text-sm"
|
className="flex items-center gap-2 rounded px-1.5 py-1.5 hover:bg-muted text-left text-sm"
|
||||||
>
|
>
|
||||||
<Plus className="h-3.5 w-3.5 text-[#1A463C]" />
|
<Plus className="h-3.5 w-3.5 text-primary" />
|
||||||
Crea <span className="font-medium">«{query.trim()}»</span>
|
Crea <span className="font-medium">«{query.trim()}»</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{filtered.length === 0 && !q && (
|
{filtered.length === 0 && !q && (
|
||||||
<p className="text-xs text-[#71717a] px-1.5 py-1">Nessuna opzione ancora</p>
|
<p className="text-xs text-muted-foreground px-1.5 py-1">Nessuna opzione ancora</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user