diff --git a/src/app/dashboard/clients/[id]/page.tsx b/src/app/dashboard/clients/[id]/page.tsx index 96eae51..9f6a8a4 100644 --- a/src/app/dashboard/clients/[id]/page.tsx +++ b/src/app/dashboard/clients/[id]/page.tsx @@ -208,7 +208,7 @@ export default async function ClientDetailPage({ {/* Recent Invoices */} {client.invoices && client.invoices.length > 0 && ( - +
@@ -222,7 +222,7 @@ export default async function ClientDetailPage({ {client.invoices.slice(0, 3).map((invoice) => (

diff --git a/src/app/dashboard/invoices/import/page.tsx b/src/app/dashboard/invoices/import/page.tsx index 6be64bb..159da41 100644 --- a/src/app/dashboard/invoices/import/page.tsx +++ b/src/app/dashboard/invoices/import/page.tsx @@ -4,8 +4,8 @@ import { HydrateClient } from "~/trpc/server"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; -import { Separator } from "~/components/ui/separator"; import { PageHeader } from "~/components/layout/page-header"; +import { CSVImportPage } from "~/components/csv-import-page"; import { ArrowLeft, Upload, @@ -14,133 +14,10 @@ import { CheckCircle, AlertCircle, Info, - Zap, FileSpreadsheet, - Eye, - RefreshCw, } from "lucide-react"; -// Import Statistics Component -function ImportStats() { - const stats = [ - { - title: "Supported Formats", - value: "CSV", - icon: FileSpreadsheet, - color: "text-blue-600", - bgColor: "bg-blue-50 dark:bg-blue-900/20", - description: "Excel & Google Sheets exports", - }, - { - title: "Max File Size", - value: "10MB", - icon: Upload, - color: "text-green-600", - bgColor: "bg-green-50 dark:bg-green-900/20", - description: "Up to 1000 invoices", - }, - { - title: "Processing Time", - value: "< 1min", - icon: Zap, - color: "text-purple-600", - bgColor: "bg-purple-50 dark:bg-purple-900/20", - description: "Average processing speed", - }, - { - title: "Success Rate", - value: "99.9%", - icon: CheckCircle, - color: "text-emerald-600", - bgColor: "bg-emerald-50 dark:bg-emerald-900/20", - description: "Import success rate", - }, - ]; - - return ( -

- {stats.map((stat) => { - const Icon = stat.icon; - return ( - - -
-
-

- {stat.title} -

-

{stat.value}

-

- {stat.description} -

-
-
- -
-
-
-
- ); - })} -
- ); -} - -// File Upload Component -function FileUploadArea() { - return ( - - - - - Upload CSV File - - - -
- {/* Drop Zone */} -
-
- -
-

- Drop your CSV file here -

-

- or click to browse and select a file -

- -

- Maximum file size: 10MB • Supported format: CSV -

-
- - {/* Upload Progress (hidden by default) */} -
-
- Uploading... - 75% -
-
-
-
-
-
-
-
- ); -} - -// CSV Format Instructions +// File Upload Instructions Component function FormatInstructions() { return (
@@ -155,7 +32,7 @@ function FormatInstructions() {

- client_name,client_email,invoice_number,issue_date,due_date,description,hours,rate,tax_rate + DATE,DESCRIPTION,HOURS,RATE,AMOUNT

@@ -163,14 +40,14 @@ function FormatInstructions() {

Required Columns:

{[ - { field: "client_name", desc: "Full name of the client" }, - { field: "client_email", desc: "Client email address" }, - { field: "invoice_number", desc: "Unique invoice identifier" }, - { field: "issue_date", desc: "Date issued (YYYY-MM-DD)" }, - { field: "due_date", desc: "Payment due date (YYYY-MM-DD)" }, - { field: "description", desc: "Work description" }, - { field: "hours", desc: "Number of hours worked" }, - { field: "rate", desc: "Hourly rate (decimal)" }, + { field: "DATE", desc: "Date of work (M/DD/YY format)" }, + { field: "DESCRIPTION", desc: "Description of work performed" }, + { field: "HOURS", desc: "Number of hours worked" }, + { field: "RATE", desc: "Hourly rate (decimal)" }, + { + field: "AMOUNT", + desc: "Total amount (calculated from hours × rate)", + }, ].map((col) => (
{col.field} @@ -183,12 +60,14 @@ function FormatInstructions() {
-

Optional Columns:

-
- tax_rate - notes - client_phone -
+

File Naming:

+

+ Name your CSV files in{" "} + + YYYY-MM-DD.csv + {" "} + format for automatic date detection. +

@@ -204,7 +83,7 @@ function FormatInstructions() {

Download our sample CSV template to see the exact format required - for importing invoices. + for importing time entries.

@@ -220,31 +99,21 @@ function FormatInstructions() {
-
- - - -
- - -

Sample Row:

- "Acme - Corp","john@acme.com","INV-001","2024-01-15","2024-02-14","Web - development - work","40","75.00","8.5" + 1/15/24,"Web development work",8,75.00,600.00

+ +
+

Sample Filename:

+
+

2024-01-15.csv

+
+
@@ -266,18 +135,18 @@ function ImportantNotes() {

Before Importing:

    -
  • • Ensure all client emails are valid
  • -
  • • Use YYYY-MM-DD format for dates
  • -
  • • Invoice numbers must be unique
  • -
  • • Rates should be in decimal format (e.g., 75.50)
  • +
  • • Use M/DD/YY format for dates (e.g., 1/15/24)
  • +
  • • Ensure rates are in decimal format (e.g., 75.50)
  • +
  • • File names should follow YYYY-MM-DD.csv format
  • +
  • • Select a client before importing

What Happens:

    -
  • • New clients will be created automatically
  • -
  • • Existing clients will be matched by email
  • -
  • • Invoices will be created in "draft" status
  • +
  • • Each CSV file creates one invoice
  • +
  • • Invoice dates are derived from filename
  • +
  • • Invoices are created in "draft" status
  • • You can review and edit before sending
@@ -287,124 +156,47 @@ function ImportantNotes() { ); } -// Import History Component -function ImportHistory() { - const mockHistory = [ - { - id: "1", - filename: "january_invoices.csv", - date: "2024-01-15", - status: "completed", - imported: 25, - errors: 0, - }, - { - id: "2", - filename: "december_invoices.csv", - date: "2024-01-01", - status: "completed", - imported: 18, - errors: 2, - }, - { - id: "3", - filename: "november_invoices.csv", - date: "2023-12-01", - status: "completed", - imported: 32, - errors: 1, - }, - ]; - - const getStatusBadge = (status: string) => { - if (status === "completed") { - return ( - - - Completed - - ); - } - if (status === "processing") { - return ( - - - Processing - - ); - } - return ( - - - Failed - - ); - }; - +// File Format Help Section +function FileFormatHelp() { return ( - - - Recent Imports + + + Supported File Formats - -
- - - - - - - - - - - - - {mockHistory.map((item) => ( - - - - - - - - - ))} - -
FileDateStatusImportedErrorsActions
-
-
- -
- {item.filename} -
-
- {new Date(item.date).toLocaleDateString()} - {getStatusBadge(item.status)} - {item.imported} - - {item.errors > 0 ? ( - {item.errors} - ) : ( - 0 - )} - - -
-
- {mockHistory.length === 0 && ( -
-

No import history yet

+ +
+
+
+ +
+

CSV Files

+

+ Comma-separated values from Excel, Google Sheets, or any CSV + editor +

- )} +
+
+ +
+

Max Size

+

+ Up to 10MB per file with no limit on number of rows +

+
+
+
+ +
+

Validation

+

+ Real-time validation with clear error messages and feedback +

+
+
); @@ -414,8 +206,8 @@ export default async function ImportPage() { return (
@@ -427,33 +219,17 @@ export default async function ImportPage() { - - {Array.from({ length: 4 }, (_, i) => ( - - -
-
-
-
-
-
-
- ))} -
- } - > - - + {/* Main CSV Import Component */} + - + {/* File Format Help */} + + {/* Format Instructions */} + {/* Important Notes */} - -
); diff --git a/src/app/dashboard/settings/_components/settings-content.tsx b/src/app/dashboard/settings/_components/settings-content.tsx index 7be2927..8b84bd8 100644 --- a/src/app/dashboard/settings/_components/settings-content.tsx +++ b/src/app/dashboard/settings/_components/settings-content.tsx @@ -10,10 +10,12 @@ import { Database, AlertTriangle, Shield, - Settings, FileText, Users, Building, + Key, + Eye, + EyeOff, } from "lucide-react"; import { api } from "~/trpc/react"; @@ -58,6 +60,14 @@ export function SettingsContent() { const [importData, setImportData] = useState(""); const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); + // Password change state + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [showCurrentPassword, setShowCurrentPassword] = useState(false); + const [showNewPassword, setShowNewPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + // Queries const { data: profile, refetch: refetchProfile } = api.settings.getProfile.useQuery(); @@ -74,6 +84,18 @@ export function SettingsContent() { }, }); + const changePasswordMutation = api.settings.changePassword.useMutation({ + onSuccess: () => { + toast.success("Password changed successfully"); + setCurrentPassword(""); + setNewPassword(""); + setConfirmPassword(""); + }, + onError: (error: { message: string }) => { + toast.error(`Failed to change password: ${error.message}`); + }, + }); + const exportDataQuery = api.settings.exportData.useQuery(undefined, { enabled: false, }); @@ -134,6 +156,27 @@ export function SettingsContent() { updateProfileMutation.mutate({ name: name.trim() }); }; + const handleChangePassword = (e: React.FormEvent) => { + e.preventDefault(); + if (!currentPassword || !newPassword || !confirmPassword) { + toast.error("Please fill in all password fields"); + return; + } + if (newPassword !== confirmPassword) { + toast.error("New passwords don't match"); + return; + } + if (newPassword.length < 8) { + toast.error("New password must be at least 8 characters"); + return; + } + changePasswordMutation.mutate({ + currentPassword, + newPassword, + confirmPassword, + }); + }; + const handleExportData = () => { void exportDataQuery.refetch(); }; @@ -239,21 +282,21 @@ export function SettingsContent() { value: dataStats?.clients ?? 0, icon: Users, color: "text-blue-600", - bgColor: "bg-blue-100", + bgColor: "bg-blue-50 dark:bg-blue-900/20", }, { label: "Businesses", value: dataStats?.businesses ?? 0, icon: Building, color: "text-purple-600", - bgColor: "bg-purple-100", + bgColor: "bg-purple-50 dark:bg-purple-900/20", }, { label: "Invoices", value: dataStats?.invoices ?? 0, icon: FileText, color: "text-emerald-600", - bgColor: "bg-emerald-100", + bgColor: "bg-emerald-50 dark:bg-emerald-900/20", }, ]; @@ -264,10 +307,8 @@ export function SettingsContent() { {/* Profile Section */} - -
- -
+ + Profile Information @@ -283,7 +324,6 @@ export function SettingsContent() { value={name} onChange={(e) => setName(e.target.value)} placeholder="Enter your full name" - className="border-0 shadow-sm" />
@@ -292,7 +332,7 @@ export function SettingsContent() { id="email" value={session?.user?.email ?? ""} disabled - className="bg-muted border-0 shadow-sm" + className="bg-muted" />

Email address cannot be changed @@ -314,10 +354,8 @@ export function SettingsContent() { {/* Data Overview */} - -

- -
+ + Account Data @@ -329,24 +367,23 @@ export function SettingsContent() { {dataStatItems.map((item) => { const Icon = item.icon; return ( - - -
-
-
- -
- {item.label} -
- - {item.value} - +
+
+
+
- - + {item.label} +
+ + {item.value} + +
); })}
@@ -354,13 +391,118 @@ export function SettingsContent() {
+ {/* Security Settings */} + + + + + Security Settings + + + Change your password and manage account security + + + +
+
+ +
+ setCurrentPassword(e.target.value)} + placeholder="Enter your current password" + /> + +
+
+ +
+ +
+ setNewPassword(e.target.value)} + placeholder="Enter your new password" + /> + +
+

+ Password must be at least 8 characters long +

+
+ +
+ +
+ setConfirmPassword(e.target.value)} + placeholder="Confirm your new password" + /> + +
+
+ + +
+
+
+ {/* Data Management */} - -
- -
+ + Data Management @@ -429,38 +571,38 @@ export function SettingsContent() {
-
- {/* Backup Information */} -
-

Backup Information

-
    -
  • • Regular backups protect your important business data
  • -
  • • Backup files contain all data in secure JSON format
  • -
  • • Import adds to existing data without replacing anything
  • -
  • • Store backup files in a secure, accessible location
  • -
+ {/* Backup Information */} +
+

Backup Information

+
    +
  • • Regular backups protect your important business data
  • +
  • • Backup files contain all data in secure JSON format
  • +
  • + • Import adds to existing data without replacing anything +
  • +
  • • Store backup files in a secure, accessible location
  • +
+
{/* Danger Zone */} - + - -
- -
- Data Management + + + Danger Zone - Manage your account data with caution + Irreversible actions that permanently affect your account
-
-

+
+

Delete All Account Data

@@ -472,7 +614,7 @@ export function SettingsContent() { - @@ -485,7 +627,7 @@ export function SettingsContent() { This action cannot be undone. This will permanently delete all your:

-
    +
    • Client information and contact details
    • Business profiles and settings
    • Invoices and invoice line items
    • @@ -516,7 +658,7 @@ export function SettingsContent() { deleteConfirmText !== "delete all my data" || deleteDataMutation.isPending } - className="btn-danger" + className="bg-red-600 hover:bg-red-700" > {deleteDataMutation.isPending ? "Deleting..." diff --git a/src/components/csv-import-page.tsx b/src/components/csv-import-page.tsx index 9dd1dfa..991f989 100644 --- a/src/components/csv-import-page.tsx +++ b/src/components/csv-import-page.tsx @@ -440,8 +440,8 @@ export function CSVImportPage() { {/* Global Client Selection */} - - + + Default Client @@ -460,7 +460,7 @@ export function CSVImportPage() { applyGlobalClient(newClientId); } }} - className="h-12 w-full rounded-md border px-3 py-2" + className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-12 w-full rounded-md border px-3 py-2 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50" disabled={loadingClients} > @@ -470,7 +470,7 @@ export function CSVImportPage() { ))} -

      +

      This client will be automatically selected for all uploaded files. You can still change individual files below.

      @@ -481,8 +481,8 @@ export function CSVImportPage() { {/* File Upload Area */} - - + + Upload CSV Files @@ -496,38 +496,55 @@ export function CSVImportPage() { description="Files must be named YYYY-MM-DD.csv (e.g., 2024-01-15.csv). Up to 50 files can be uploaded at once." /> - {/* Summary Stats */} + {/* Summary Card */} {totalFiles > 0 && ( -
      -
      -
      - {totalFiles} + + + + + Import Summary + + + +
      +
      +
      + {totalFiles} +
      +
      + Files +
      +
      +
      +
      + {totalItems} +
      +
      + Total Items +
      +
      +
      +
      + {totalAmount.toLocaleString("en-US", { + style: "currency", + currency: "USD", + })} +
      +
      + Total Amount +
      +
      +
      +
      + {readyFiles}/{totalFiles} +
      +
      + Ready +
      +
      -
      Files
      -
      of 50 max
      -
      -
      -
      - {totalItems} -
      -
      Total Items
      -
      -
      -
      - {totalAmount.toLocaleString("en-US", { - style: "currency", - currency: "USD", - })} -
      -
      Total Amount
      -
      -
      -
      - {readyFiles}/{totalFiles} -
      -
      Ready
      -
      -
      + + )} @@ -536,23 +553,25 @@ export function CSVImportPage() { {files.length > 0 && ( - Uploaded Files + + Uploaded Files +
      {files.map((fileData, index) => (
      -

      +

      {fileData.file.name}

      -

      +

      {fileData.parsedItems.length} items •{" "} {fileData.parsedItems .reduce((sum, item) => sum + item.hours, 0) @@ -574,7 +593,7 @@ export function CSVImportPage() { variant="outline" size="sm" onClick={() => removeFile(index)} - className="text-icon-red hover:text-error" + className="text-red-600 hover:text-red-700" > Remove @@ -584,7 +603,7 @@ export function CSVImportPage() {

      -