Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,7 @@
"cancel": "Abbrechen",
"tabs": {
"all": "Übersicht",
"proposals": "Angebote",
"shoots": "Shootings & Sessions",
"locations": "Orte & Venues",
"timeline": "Ablauf & Zeitplan",
Expand Down Expand Up @@ -1870,6 +1871,42 @@
"incompleteAria": "Als erledigt markieren",
"submit": "Aufgabe hinzufügen",
"submitting": "Wird hinzugefügt..."
},
"proposals": {
"title": "Kostenvoranschläge & Angebote",
"description": "Kommerzielle Angebote, Paketauswahlen und Kundenannahme-Links für dieses Projekt.",
"newProposal": "Neues Angebot",
"empty": "Noch keine Angebote für dieses Projekt erstellt.",
"version": "Version v{version}",
"createdAt": "Erstellt am {date}",
"total": "Gesamtbetrag",
"validUntil": "Gültig bis {date}",
"acceptedOn": "Angenommen am {date}",
"noExpiration": "Keine Befristung",
"statuses": {
"draft": "Entwurf",
"sent": "Versendet",
"accepted": "Angenommen",
"declined": "Abgelehnt",
"expired": "Abgelaufen",
"superseded": "Ersetzt"
},
"actions": {
"editDraft": "Entwurf bearbeiten",
"copyPublicLink": "Öffentlichen Link kopieren",
"linkCopied": "Link kopiert!",
"revokeLink": "Link widerrufen",
"linkRevoked": "Link widerrufen",
"copyError": "Link konnte nicht in die Zwischenablage kopiert werden.",
"revokeError": "Link konnte nicht widerrufen werden."
},
"revokeDialog": {
"title": "Öffentlichen Link widerrufen",
"description": "Möchten Sie den öffentlichen Link für dieses Angebot wirklich widerrufen? Der Kunde verliert sofort den Zugriff und kann das Angebot nicht mehr online einsehen oder annehmen.",
"cancel": "Abbrechen",
"confirm": "Link widerrufen",
"revoking": "Wird widerrufen..."
}
}
}
},
Expand Down
37 changes: 37 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,7 @@
"cancel": "Cancel",
"tabs": {
"all": "Overview",
"proposals": "Proposals",
"shoots": "Shoots & Sessions",
"locations": "Locations",
"timeline": "Timeline",
Expand Down Expand Up @@ -1870,6 +1871,42 @@
"incompleteAria": "Mark as completed",
"submit": "Add Task",
"submitting": "Adding..."
},
"proposals": {
"title": "Quotes & Proposals",
"description": "Commercial proposals, package selections, and client acceptance links for this project.",
"newProposal": "New Proposal",
"empty": "No proposals created for this project yet.",
"version": "Revision v{version}",
"createdAt": "Created on {date}",
"total": "Total Amount",
"validUntil": "Valid until {date}",
"acceptedOn": "Accepted on {date}",
"noExpiration": "No expiration date",
"statuses": {
"draft": "Draft",
"sent": "Sent",
"accepted": "Accepted",
"declined": "Declined",
"expired": "Expired",
"superseded": "Superseded"
},
"actions": {
"editDraft": "Edit Draft",
"copyPublicLink": "Copy Public Link",
"linkCopied": "Link Copied!",
"revokeLink": "Revoke Link",
"linkRevoked": "Link Revoked",
"copyError": "Failed to copy link to clipboard.",
"revokeError": "Failed to revoke proposal link."
},
"revokeDialog": {
"title": "Revoke Public Link",
"description": "Are you sure you want to revoke the public link for this proposal? The client will immediately lose access and will no longer be able to view or accept this proposal online.",
"cancel": "Cancel",
"confirm": "Revoke Link",
"revoking": "Revoking..."
}
}
}
},
Expand Down
7 changes: 7 additions & 0 deletions src/components/clients/client-timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
Mail,
FolderKanban,
ArrowRightCircle,
CheckCircle2,
} from 'lucide-react';
import type { ResolvedTimelineItem } from '@/server/timeline/repo';
import { formatDateTime } from '@/lib/formatters';
Expand Down Expand Up @@ -357,11 +358,17 @@ export function ClientTimeline({
{event.type === 'project.stage_changed' && (
<ArrowRightCircle className="h-4 w-4 text-blue-600 dark:text-blue-400" />
)}
{event.type === 'proposal.accepted' && (
<CheckCircle2 className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
)}
{isNote && <FileText className="h-4 w-4 text-blue-600 dark:text-blue-400" />}
</span>

<div className="space-y-1 flex-1">
<div className="flex flex-wrap items-center gap-1.5 font-medium text-foreground">
{event.type === 'proposal.accepted' && (
<span>{t('events.proposal_accepted')}</span>
)}
{event.type === 'client.created' && (
<span>{t('events.client_created')}</span>
)}
Expand Down
47 changes: 45 additions & 2 deletions src/components/projects/project-detail-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
transitionProjectStageAction,
} from '@/server/projects/actions';
import { TaskWarningDialog, type IncompleteTaskItem } from './task-warning-dialog';
import { ProposalsSection } from './proposals-section';
import { ShootsSection } from './shoots-section';
import { LocationsSection } from './locations-section';
import { TimelineScheduleSection } from './timeline-schedule-section';
Expand All @@ -49,7 +50,7 @@ export interface ProjectDetailViewProps {
locale: string;
}

type TabKey = 'all' | 'shoots' | 'locations' | 'timeline' | 'participants' | 'tasks';
type TabKey = 'all' | 'proposals' | 'shoots' | 'locations' | 'timeline' | 'participants' | 'tasks';

const PROJECT_TYPES = [
'wedding',
Expand Down Expand Up @@ -261,6 +262,11 @@ export function ProjectDetailView({ initialData, locale }: ProjectDetailViewProp
// Tabs metadata
const tabList: { key: TabKey; label: string; count?: number }[] = [
{ key: 'all', label: tDetail('tabs.all') },
{
key: 'proposals',
label: tDetail('tabs.proposals'),
count: initialData.proposals?.length ?? 0,
},
{ key: 'shoots', label: tDetail('tabs.shoots'), count: shoots.length },
{ key: 'locations', label: tDetail('tabs.locations'), count: locations.length },
{ key: 'timeline', label: tDetail('tabs.timeline'), count: timelineItems.length },
Expand Down Expand Up @@ -430,7 +436,7 @@ export function ProjectDetailView({ initialData, locale }: ProjectDetailViewProp
aria-label="Project sections"
className="flex flex-wrap gap-2 -mb-px overflow-x-auto py-1"
>
{tabList.map((tab) => {
{tabList.map((tab, idx) => {
const isActive = activeTab === tab.key;
return (
<button
Expand All @@ -445,6 +451,28 @@ export function ProjectDetailView({ initialData, locale }: ProjectDetailViewProp
onClick={() => {
setActiveTab(tab.key);
}}
onKeyDown={(e) => {
let nextIndex: number | null = null;
if (e.key === 'ArrowRight') {
nextIndex = (idx + 1) % tabList.length;
} else if (e.key === 'ArrowLeft') {
nextIndex = (idx - 1 + tabList.length) % tabList.length;
} else if (e.key === 'Home') {
nextIndex = 0;
} else if (e.key === 'End') {
nextIndex = tabList.length - 1;
}

if (nextIndex !== null) {
e.preventDefault();
const nextTab = tabList[nextIndex];
if (nextTab) {
setActiveTab(nextTab.key);
const el = document.getElementById(`tab-${nextTab.key}`);
el?.focus();
}
}
}}
className={cn(
'inline-flex items-center gap-2 px-3.5 py-2 text-sm font-medium border-b-2 transition-colors min-h-[44px] sm:min-h-[36px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-t-md',
isActive
Expand Down Expand Up @@ -473,6 +501,11 @@ export function ProjectDetailView({ initialData, locale }: ProjectDetailViewProp
<main className="space-y-8">
{activeTab === 'all' && (
<div id="panel-all" role="tabpanel" aria-labelledby="tab-all" className="space-y-10">
<ProposalsSection
projectId={project.id}
initialProposals={initialData.proposals ?? []}
locale={locale}
/>
<ShootsSection
projectId={project.id}
shoots={shoots}
Expand Down Expand Up @@ -511,6 +544,16 @@ export function ProjectDetailView({ initialData, locale }: ProjectDetailViewProp
</div>
)}

{activeTab === 'proposals' && (
<div id="panel-proposals" role="tabpanel" aria-labelledby="tab-proposals">
<ProposalsSection
projectId={project.id}
initialProposals={initialData.proposals ?? []}
locale={locale}
/>
</div>
)}

{activeTab === 'shoots' && (
<div id="panel-shoots" role="tabpanel" aria-labelledby="tab-shoots">
<ShootsSection
Expand Down
Loading
Loading