From 4c546d654a1208df056c4c79fff149412e1611a9 Mon Sep 17 00:00:00 2001 From: Jake Shore Date: Thu, 12 Feb 2026 23:57:44 -0500 Subject: [PATCH] housecall-pro: Add 15 React MCP apps --- .../react-app/src/apps/customer-grid/App.tsx | 150 +++++++++ .../src/apps/customer-grid/index.html | 12 + .../react-app/src/apps/customer-grid/main.tsx | 10 + .../src/apps/customer-grid/styles.css | 208 ++++++++++++ .../react-app/src/apps/dispatch-board/App.tsx | 202 ++++++++++++ .../src/apps/dispatch-board/index.html | 12 + .../src/apps/dispatch-board/main.tsx | 10 + .../src/apps/dispatch-board/styles.css | 282 ++++++++++++++++ .../react-app/src/apps/job-dashboard/App.tsx | 108 ++++++ .../src/apps/job-dashboard/index.html | 12 + .../react-app/src/apps/job-dashboard/main.tsx | 10 + .../src/apps/job-dashboard/styles.css | 170 ++++++++++ .../ui/react-app/src/apps/job-grid/App.tsx | 153 +++++++++ .../ui/react-app/src/apps/job-grid/index.html | 12 + .../ui/react-app/src/apps/job-grid/main.tsx | 10 + .../ui/react-app/src/apps/job-grid/styles.css | 208 ++++++++++++ .../src/apps/payment-history/App.tsx | 181 ++++++++++ .../src/apps/payment-history/index.html | 12 + .../src/apps/payment-history/main.tsx | 10 + .../src/apps/payment-history/styles.css | 209 ++++++++++++ .../src/apps/revenue-dashboard/App.tsx | 192 +++++++++++ .../src/apps/revenue-dashboard/index.html | 12 + .../src/apps/revenue-dashboard/main.tsx | 10 + .../src/apps/revenue-dashboard/styles.css | 310 ++++++++++++++++++ .../react-app/src/apps/review-tracker/App.tsx | 198 +++++++++++ .../src/apps/review-tracker/index.html | 12 + .../src/apps/review-tracker/main.tsx | 10 + .../src/apps/review-tracker/styles.css | 296 +++++++++++++++++ .../src/apps/technician-dashboard/App.tsx | 177 ++++++++++ .../src/apps/technician-dashboard/index.html | 12 + .../src/apps/technician-dashboard/main.tsx | 10 + .../src/apps/technician-dashboard/styles.css | 308 +++++++++++++++++ 32 files changed, 3528 insertions(+) create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/App.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/index.html create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/main.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/styles.css create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/App.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/index.html create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/main.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/styles.css create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/App.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/index.html create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/main.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/styles.css create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/job-grid/App.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/job-grid/index.html create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/job-grid/main.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/job-grid/styles.css create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/payment-history/App.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/payment-history/index.html create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/payment-history/main.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/payment-history/styles.css create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/App.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/index.html create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/main.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/styles.css create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/App.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/index.html create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/main.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/styles.css create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/App.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/index.html create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/main.tsx create mode 100644 servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/styles.css diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/App.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/App.tsx new file mode 100644 index 0000000..5bfc256 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/App.tsx @@ -0,0 +1,150 @@ +/** + * Customer Grid - Searchable customer directory + */ + +import React, { useState, useMemo } from 'react'; +import './styles.css'; + +interface Customer { + id: string; + name: string; + email: string; + phone: string; + address: string; + city: string; + totalJobs: number; + totalRevenue: number; + lastService: string; + status: 'active' | 'inactive' | 'vip'; +} + +const mockCustomers: Customer[] = [ + { id: 'C-1001', name: 'John Smith', email: 'john.smith@email.com', phone: '(555) 123-4567', address: '123 Main St', city: 'Springfield', totalJobs: 12, totalRevenue: 4500, lastService: '2024-02-10', status: 'active' }, + { id: 'C-1002', name: 'Sarah Johnson', email: 'sarah.j@email.com', phone: '(555) 234-5678', address: '456 Oak Ave', city: 'Riverside', totalJobs: 24, totalRevenue: 12800, lastService: '2024-02-12', status: 'vip' }, + { id: 'C-1003', name: 'Mike Williams', email: 'mike.w@email.com', phone: '(555) 345-6789', address: '789 Pine Dr', city: 'Lakeside', totalJobs: 3, totalRevenue: 890, lastService: '2024-01-15', status: 'active' }, + { id: 'C-1004', name: 'Emily Davis', email: 'emily.davis@email.com', phone: '(555) 456-7890', address: '321 Elm St', city: 'Springfield', totalJobs: 8, totalRevenue: 3200, lastService: '2024-02-08', status: 'active' }, + { id: 'C-1005', name: 'Robert Martinez', email: 'r.martinez@email.com', phone: '(555) 567-8901', address: '654 Maple Ln', city: 'Hillside', totalJobs: 18, totalRevenue: 8900, lastService: '2024-02-11', status: 'vip' }, + { id: 'C-1006', name: 'Lisa Anderson', email: 'lisa.a@email.com', phone: '(555) 678-9012', address: '987 Cedar Ct', city: 'Riverside', totalJobs: 5, totalRevenue: 1750, lastService: '2024-01-28', status: 'active' }, + { id: 'C-1007', name: 'David Thompson', email: 'david.t@email.com', phone: '(555) 789-0123', address: '147 Birch Rd', city: 'Lakeside', totalJobs: 1, totalRevenue: 250, lastService: '2023-12-05', status: 'inactive' }, + { id: 'C-1008', name: 'Jennifer Garcia', email: 'jen.garcia@email.com', phone: '(555) 890-1234', address: '258 Willow Way', city: 'Springfield', totalJobs: 15, totalRevenue: 6200, lastService: '2024-02-09', status: 'active' }, + { id: 'C-1009', name: 'William Brown', email: 'w.brown@email.com', phone: '(555) 901-2345', address: '369 Spruce St', city: 'Hillside', totalJobs: 2, totalRevenue: 450, lastService: '2023-11-20', status: 'inactive' }, + { id: 'C-1010', name: 'Amanda Wilson', email: 'amanda.w@email.com', phone: '(555) 012-3456', address: '741 Ash Blvd', city: 'Riverside', totalJobs: 28, totalRevenue: 15600, lastService: '2024-02-12', status: 'vip' } +]; + +export default function CustomerGrid({ api }: any) { + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [cityFilter, setCityFilter] = useState('all'); + + const cities = useMemo(() => { + const uniqueCities = [...new Set(mockCustomers.map(c => c.city))]; + return uniqueCities.sort(); + }, []); + + const filteredCustomers = useMemo(() => { + return mockCustomers.filter(customer => { + const matchesSearch = searchTerm === '' || + customer.name.toLowerCase().includes(searchTerm.toLowerCase()) || + customer.email.toLowerCase().includes(searchTerm.toLowerCase()) || + customer.phone.includes(searchTerm) || + customer.address.toLowerCase().includes(searchTerm.toLowerCase()) || + customer.id.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesStatus = statusFilter === 'all' || customer.status === statusFilter; + const matchesCity = cityFilter === 'all' || customer.city === cityFilter; + + return matchesSearch && matchesStatus && matchesCity; + }); + }, [searchTerm, statusFilter, cityFilter]); + + const getStatusClass = (status: string) => { + const classes: Record = { + 'active': 'status-active', + 'inactive': 'status-inactive', + 'vip': 'status-vip' + }; + return classes[status] || ''; + }; + + return ( +
+
+

Customer Directory

+
+ setSearchTerm(e.target.value)} + className="search-input" + /> +
+
+ +
+
+ + +
+
+ + +
+
+ {filteredCustomers.length} customer{filteredCustomers.length !== 1 ? 's' : ''} +
+
+ +
+ + + + + + + + + + + + + + + {filteredCustomers.map(customer => ( + + + + + + + + + + + ))} + +
IDNameContactLocationTotal JobsRevenueLast ServiceStatus
{customer.id}{customer.name} +
{customer.email}
+
{customer.phone}
+
+
{customer.address}
+
{customer.city}
+
{customer.totalJobs}${customer.totalRevenue.toLocaleString()}{new Date(customer.lastService).toLocaleDateString()} + + {customer.status} + +
+
+
+ ); +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/index.html b/servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/index.html new file mode 100644 index 0000000..1c01c92 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/index.html @@ -0,0 +1,12 @@ + + + + + + Customer Grid - Housecall Pro + + +
+ + + diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/main.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/main.tsx new file mode 100644 index 0000000..0255055 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/styles.css b/servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/styles.css new file mode 100644 index 0000000..1a49e72 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/customer-grid/styles.css @@ -0,0 +1,208 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + background: #0f172a; + color: #e2e8f0; + padding: 20px; +} + +.customer-grid { + max-width: 1600px; + margin: 0 auto; +} + +.grid-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; +} + +.grid-header h1 { + font-size: 32px; + font-weight: 700; + color: #f1f5f9; +} + +.search-input { + width: 500px; + padding: 12px 16px; + background: #1e293b; + border: 1px solid #334155; + border-radius: 8px; + color: #e2e8f0; + font-size: 14px; +} + +.search-input:focus { + outline: none; + border-color: #3b82f6; +} + +.search-input::placeholder { + color: #64748b; +} + +.filters { + display: flex; + gap: 20px; + align-items: flex-end; + margin-bottom: 24px; + padding: 20px; + background: #1e293b; + border-radius: 12px; +} + +.filter-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.filter-group label { + font-size: 12px; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.filter-group select { + padding: 10px 14px; + background: #0f172a; + border: 1px solid #334155; + border-radius: 6px; + color: #e2e8f0; + font-size: 14px; + min-width: 180px; +} + +.filter-group select:focus { + outline: none; + border-color: #3b82f6; +} + +.results-count { + margin-left: auto; + padding: 10px 16px; + background: #0f172a; + border-radius: 6px; + color: #94a3b8; + font-size: 14px; +} + +.table-container { + background: #1e293b; + border-radius: 12px; + overflow: hidden; + border: 1px solid #334155; +} + +.customers-table { + width: 100%; + border-collapse: collapse; +} + +.customers-table thead { + background: #0f172a; +} + +.customers-table th { + padding: 16px; + text-align: left; + font-size: 12px; + font-weight: 600; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 1px solid #334155; +} + +.customers-table tbody tr { + border-bottom: 1px solid #334155; + transition: background 0.15s; +} + +.customers-table tbody tr:hover { + background: rgba(59, 130, 246, 0.05); + cursor: pointer; +} + +.customers-table tbody tr:last-child { + border-bottom: none; +} + +.customers-table td { + padding: 16px; + font-size: 14px; + color: #e2e8f0; +} + +.customer-id { + color: #3b82f6; + font-weight: 600; +} + +.customer-name { + font-weight: 600; + color: #f1f5f9; + font-size: 15px; +} + +.contact-info { + line-height: 1.6; +} + +.contact-info .phone { + color: #94a3b8; + font-size: 13px; +} + +.location { + line-height: 1.6; +} + +.location .city { + color: #94a3b8; + font-size: 13px; +} + +.jobs-count { + font-weight: 600; + color: #60a5fa; + text-align: center; +} + +.revenue { + font-weight: 600; + color: #10b981; + text-align: right; +} + +.status-badge { + padding: 4px 12px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + display: inline-block; +} + +.status-active { + background: rgba(16, 185, 129, 0.15); + color: #34d399; +} + +.status-vip { + background: rgba(168, 85, 247, 0.15); + color: #c084fc; +} + +.status-inactive { + background: rgba(148, 163, 184, 0.15); + color: #94a3b8; +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/App.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/App.tsx new file mode 100644 index 0000000..7156777 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/App.tsx @@ -0,0 +1,202 @@ +/** + * Dispatch Board - Drag-style dispatch view of technicians and jobs + */ + +import React, { useState } from 'react'; +import './styles.css'; + +interface Job { + id: string; + customer: string; + service: string; + time: string; + duration: number; + value: number; + priority: 'low' | 'medium' | 'high'; +} + +interface Technician { + id: string; + name: string; + status: 'available' | 'busy' | 'offline'; + jobs: Job[]; + capacity: number; +} + +const mockTechnicians: Technician[] = [ + { + id: 'T-001', + name: 'Mike Johnson', + status: 'busy', + capacity: 8, + jobs: [ + { id: 'J-1001', customer: 'Smith Residence', service: 'HVAC Repair', time: '09:00', duration: 2, value: 450, priority: 'medium' }, + { id: 'J-1004', customer: 'Martinez Home', service: 'AC Maintenance', time: '14:00', duration: 1.5, value: 150, priority: 'low' } + ] + }, + { + id: 'T-002', + name: 'Sarah Lee', + status: 'busy', + capacity: 8, + jobs: [ + { id: 'J-1002', customer: 'Downtown Office', service: 'Plumbing Install', time: '10:30', duration: 3, value: 1200, priority: 'high' }, + { id: 'J-1005', customer: 'Riverside Complex', service: 'Water Heater Repair', time: '15:30', duration: 2, value: 380, priority: 'high' } + ] + }, + { + id: 'T-003', + name: 'Tom Wilson', + status: 'available', + capacity: 8, + jobs: [ + { id: 'J-1003', customer: 'Oak Street Apt', service: 'Electrical Inspection', time: '12:00', duration: 1, value: 200, priority: 'low' } + ] + }, + { + id: 'T-004', + name: 'Jessica Martinez', + status: 'available', + capacity: 8, + jobs: [] + }, + { + id: 'T-005', + name: 'David Chen', + status: 'offline', + capacity: 8, + jobs: [] + } +]; + +const unassignedJobs: Job[] = [ + { id: 'J-2001', customer: 'Harbor View', service: 'Drain Cleaning', time: '13:00', duration: 1, value: 175, priority: 'medium' }, + { id: 'J-2002', customer: 'Pine Street House', service: 'Furnace Repair', time: '11:00', duration: 2.5, value: 620, priority: 'high' }, + { id: 'J-2003', customer: 'Valley Office Park', service: 'Commercial HVAC', time: '16:00', duration: 4, value: 2400, priority: 'high' } +]; + +export default function DispatchBoard({ api }: any) { + const [technicians, setTechnicians] = useState(mockTechnicians); + const [unassigned, setUnassigned] = useState(unassignedJobs); + + const getStatusClass = (status: string) => { + const classes: Record = { + 'available': 'status-available', + 'busy': 'status-busy', + 'offline': 'status-offline' + }; + return classes[status] || ''; + }; + + const getPriorityClass = (priority: string) => { + const classes: Record = { + 'low': 'priority-low', + 'medium': 'priority-medium', + 'high': 'priority-high' + }; + return classes[priority] || ''; + }; + + const getTotalHours = (jobs: Job[]) => { + return jobs.reduce((sum, job) => sum + job.duration, 0); + }; + + const getTotalRevenue = (jobs: Job[]) => { + return jobs.reduce((sum, job) => sum + job.value, 0); + }; + + return ( +
+
+

Dispatch Board

+
+ + 📅 {new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })} + + + {technicians.filter(t => t.status === 'available').length} Available • + {technicians.filter(t => t.status === 'busy').length} Busy • + {technicians.filter(t => t.status === 'offline').length} Offline + +
+
+ +
+
+
+

Unassigned Jobs

+ {unassigned.length} +
+
+ {unassigned.map(job => ( +
+
+ {job.id} + + {job.priority} + +
+
{job.customer}
+
{job.service}
+
+ 🕐 {job.time} + ⏱ {job.duration}h + ${job.value} +
+
+ ))} +
+
+ +
+ {technicians.map(tech => ( +
+
+
+

{tech.name}

+ + {tech.status} + +
+
+
+ {getTotalHours(tech.jobs)}/{tech.capacity}h +
+
Hours
+
+
+
+ {tech.jobs.length === 0 ? ( +
No jobs assigned
+ ) : ( + tech.jobs.map(job => ( +
+
+ {job.id} + + {job.priority} + +
+
{job.customer}
+
{job.service}
+
+ 🕐 {job.time} + ⏱ {job.duration}h + ${job.value} +
+
+ )) + )} +
+
+
+ Total: ${getTotalRevenue(tech.jobs).toLocaleString()} +
+
+
+ ))} +
+
+
+ ); +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/index.html b/servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/index.html new file mode 100644 index 0000000..884f467 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/index.html @@ -0,0 +1,12 @@ + + + + + + Dispatch Board - Housecall Pro + + +
+ + + diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/main.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/main.tsx new file mode 100644 index 0000000..0255055 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/styles.css b/servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/styles.css new file mode 100644 index 0000000..4e92f14 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/dispatch-board/styles.css @@ -0,0 +1,282 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + background: #0f172a; + color: #e2e8f0; + padding: 20px; +} + +.dispatch-board { + max-width: 100%; + margin: 0 auto; +} + +.board-header { + margin-bottom: 24px; +} + +.board-header h1 { + font-size: 32px; + font-weight: 700; + color: #f1f5f9; + margin-bottom: 8px; +} + +.header-info { + display: flex; + gap: 24px; + color: #94a3b8; + font-size: 14px; +} + +.info-item { + display: flex; + align-items: center; + gap: 8px; +} + +.board-container { + display: flex; + gap: 20px; + overflow-x: auto; + padding-bottom: 20px; +} + +.unassigned-column { + min-width: 320px; + flex-shrink: 0; +} + +.technicians-columns { + display: flex; + gap: 20px; + flex: 1; +} + +.tech-column { + min-width: 280px; + flex-shrink: 0; + display: flex; + flex-direction: column; + max-height: calc(100vh - 200px); +} + +.column-header { + background: #1e293b; + padding: 16px; + border-radius: 12px 12px 0 0; + border: 1px solid #334155; + border-bottom: none; +} + +.unassigned-header { + display: flex; + justify-content: space-between; + align-items: center; + background: #1e293b; + border-left: 4px solid #f59e0b; +} + +.unassigned-header h2 { + font-size: 18px; + color: #f1f5f9; +} + +.job-count { + background: #0f172a; + padding: 4px 12px; + border-radius: 12px; + font-weight: 600; + color: #f59e0b; +} + +.tech-header { + border-left: 4px solid #3b82f6; +} + +.tech-header.status-available { + border-left-color: #10b981; +} + +.tech-header.status-busy { + border-left-color: #f59e0b; +} + +.tech-header.status-offline { + border-left-color: #64748b; +} + +.tech-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.tech-info h3 { + font-size: 16px; + color: #f1f5f9; +} + +.status-badge { + padding: 4px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + text-transform: capitalize; +} + +.status-available { + background: rgba(16, 185, 129, 0.15); + color: #34d399; +} + +.status-busy { + background: rgba(245, 158, 11, 0.15); + color: #fbbf24; +} + +.status-offline { + background: rgba(100, 116, 139, 0.15); + color: #94a3b8; +} + +.tech-stats { + text-align: center; +} + +.stat { + font-size: 18px; + font-weight: 700; + color: #f1f5f9; +} + +.stat-label { + font-size: 11px; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.job-list { + background: #1e293b; + border: 1px solid #334155; + border-top: none; + border-radius: 0 0 12px 12px; + padding: 12px; + min-height: 200px; + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 12px; +} + +.job-card { + background: #0f172a; + padding: 14px; + border-radius: 8px; + border: 1px solid #334155; + cursor: grab; + transition: all 0.2s; +} + +.job-card:hover { + border-color: #3b82f6; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1); +} + +.job-card:active { + cursor: grabbing; +} + +.job-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.job-id { + font-size: 12px; + font-weight: 600; + color: #3b82f6; +} + +.priority-badge { + padding: 2px 8px; + border-radius: 10px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; +} + +.priority-low { + background: rgba(148, 163, 184, 0.15); + color: #94a3b8; +} + +.priority-medium { + background: rgba(245, 158, 11, 0.15); + color: #fbbf24; +} + +.priority-high { + background: rgba(239, 68, 68, 0.15); + color: #f87171; +} + +.job-customer { + font-size: 14px; + font-weight: 600; + color: #f1f5f9; + margin-bottom: 4px; +} + +.job-service { + font-size: 13px; + color: #94a3b8; + margin-bottom: 8px; +} + +.job-meta { + display: flex; + gap: 12px; + font-size: 12px; + color: #94a3b8; +} + +.job-value { + margin-left: auto; + font-weight: 600; + color: #10b981; +} + +.empty-state { + text-align: center; + padding: 40px 20px; + color: #64748b; + font-size: 14px; +} + +.tech-footer { + background: #1e293b; + border: 1px solid #334155; + border-top: none; + border-radius: 0 0 12px 12px; + padding: 12px 16px; + margin-top: -12px; +} + +.revenue-total { + text-align: center; + font-weight: 600; + color: #10b981; + font-size: 14px; +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/App.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/App.tsx new file mode 100644 index 0000000..adb3de3 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/App.tsx @@ -0,0 +1,108 @@ +/** + * Job Dashboard - Overview stats and today's schedule + */ + +import React, { useState, useEffect } from 'react'; +import './styles.css'; + +interface JobStats { + open: number; + scheduled: number; + completed: number; + inProgress: number; +} + +interface TodayJob { + id: string; + customer: string; + service: string; + time: string; + technician: string; + status: 'scheduled' | 'in-progress' | 'completed'; + value: number; +} + +export default function JobDashboard({ api }: any) { + const [loading, setLoading] = useState(true); + const [stats, setStats] = useState({ + open: 24, + scheduled: 18, + completed: 156, + inProgress: 6 + }); + const [todayJobs, setTodayJobs] = useState([ + { id: 'J-1001', customer: 'Smith Residence', service: 'HVAC Repair', time: '09:00 AM', technician: 'Mike Johnson', status: 'completed', value: 450 }, + { id: 'J-1002', customer: 'Downtown Office', service: 'Plumbing Install', time: '10:30 AM', technician: 'Sarah Lee', status: 'in-progress', value: 1200 }, + { id: 'J-1003', customer: 'Oak Street Apt', service: 'Electrical Inspection', time: '12:00 PM', technician: 'Tom Wilson', status: 'scheduled', value: 200 }, + { id: 'J-1004', customer: 'Martinez Home', service: 'AC Maintenance', time: '02:00 PM', technician: 'Mike Johnson', status: 'scheduled', value: 150 }, + { id: 'J-1005', customer: 'Riverside Complex', service: 'Water Heater Repair', time: '03:30 PM', technician: 'Sarah Lee', status: 'scheduled', value: 380 } + ]); + + useEffect(() => { + setTimeout(() => setLoading(false), 500); + }, []); + + const getStatusBadge = (status: string) => { + const badges = { + 'scheduled': 'badge-scheduled', + 'in-progress': 'badge-progress', + 'completed': 'badge-completed' + }; + return badges[status as keyof typeof badges] || 'badge-scheduled'; + }; + + if (loading) { + return
Loading dashboard...
; + } + + return ( +
+
+

Job Dashboard

+
{new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
+
+ +
+
+
{stats.open}
+
Open Jobs
+
+
+
{stats.scheduled}
+
Scheduled
+
+
+
{stats.inProgress}
+
In Progress
+
+
+
{stats.completed}
+
Completed (MTD)
+
+
+ +
+

Today's Schedule

+
+ {todayJobs.map(job => ( +
+
+
{job.id}
+ + {job.status.replace('-', ' ')} + +
+
{job.customer}
+
{job.service}
+
+
🕐 {job.time}
+
👤 {job.technician}
+
${job.value}
+
+
+ ))} +
+
+
+ ); +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/index.html b/servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/index.html new file mode 100644 index 0000000..e311b22 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Job Dashboard - Housecall Pro + + +
+ + + diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/main.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/main.tsx new file mode 100644 index 0000000..0255055 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/styles.css b/servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/styles.css new file mode 100644 index 0000000..c5b3d9c --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/job-dashboard/styles.css @@ -0,0 +1,170 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + background: #0f172a; + color: #e2e8f0; + padding: 20px; +} + +.loading { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + font-size: 18px; + color: #64748b; +} + +.job-dashboard { + max-width: 1400px; + margin: 0 auto; +} + +.dashboard-header { + margin-bottom: 32px; +} + +.dashboard-header h1 { + font-size: 32px; + font-weight: 700; + color: #f1f5f9; + margin-bottom: 8px; +} + +.date { + color: #94a3b8; + font-size: 14px; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-bottom: 40px; +} + +.stat-card { + background: #1e293b; + padding: 24px; + border-radius: 12px; + border-left: 4px solid #3b82f6; +} + +.stat-card.stat-open { border-left-color: #3b82f6; } +.stat-card.stat-scheduled { border-left-color: #8b5cf6; } +.stat-card.stat-progress { border-left-color: #f59e0b; } +.stat-card.stat-completed { border-left-color: #10b981; } + +.stat-value { + font-size: 36px; + font-weight: 700; + color: #f1f5f9; + margin-bottom: 8px; +} + +.stat-label { + color: #94a3b8; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.today-schedule h2 { + font-size: 24px; + margin-bottom: 20px; + color: #f1f5f9; +} + +.job-list { + display: grid; + gap: 16px; +} + +.job-card { + background: #1e293b; + padding: 20px; + border-radius: 12px; + border: 1px solid #334155; + transition: all 0.2s; +} + +.job-card:hover { + border-color: #3b82f6; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1); +} + +.job-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.job-id { + font-weight: 600; + color: #3b82f6; + font-size: 14px; +} + +.badge { + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + text-transform: capitalize; +} + +.badge-scheduled { + background: rgba(139, 92, 246, 0.15); + color: #a78bfa; +} + +.badge-progress { + background: rgba(245, 158, 11, 0.15); + color: #fbbf24; +} + +.badge-completed { + background: rgba(16, 185, 129, 0.15); + color: #34d399; +} + +.job-customer { + font-size: 18px; + font-weight: 600; + color: #f1f5f9; + margin-bottom: 4px; +} + +.job-service { + color: #94a3b8; + font-size: 14px; + margin-bottom: 12px; +} + +.job-footer { + display: flex; + gap: 20px; + align-items: center; + padding-top: 12px; + border-top: 1px solid #334155; + font-size: 13px; +} + +.job-time, +.job-tech { + color: #94a3b8; +} + +.job-value { + margin-left: auto; + font-weight: 600; + color: #10b981; + font-size: 16px; +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/job-grid/App.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/job-grid/App.tsx new file mode 100644 index 0000000..77a5c4f --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/job-grid/App.tsx @@ -0,0 +1,153 @@ +/** + * Job Grid - Searchable/filterable table of all jobs + */ + +import React, { useState, useMemo } from 'react'; +import './styles.css'; + +interface Job { + id: string; + customer: string; + service: string; + date: string; + technician: string; + status: 'open' | 'scheduled' | 'in-progress' | 'completed' | 'cancelled'; + value: number; + priority: 'low' | 'medium' | 'high'; +} + +const mockJobs: Job[] = [ + { id: 'J-1001', customer: 'Smith Residence', service: 'HVAC Repair', date: '2024-02-12', technician: 'Mike Johnson', status: 'completed', value: 450, priority: 'medium' }, + { id: 'J-1002', customer: 'Downtown Office', service: 'Plumbing Install', date: '2024-02-12', technician: 'Sarah Lee', status: 'in-progress', value: 1200, priority: 'high' }, + { id: 'J-1003', customer: 'Oak Street Apt', service: 'Electrical Inspection', date: '2024-02-12', technician: 'Tom Wilson', status: 'scheduled', value: 200, priority: 'low' }, + { id: 'J-1004', customer: 'Martinez Home', service: 'AC Maintenance', date: '2024-02-13', technician: 'Mike Johnson', status: 'scheduled', value: 150, priority: 'medium' }, + { id: 'J-1005', customer: 'Riverside Complex', service: 'Water Heater Repair', date: '2024-02-13', technician: 'Sarah Lee', status: 'open', value: 380, priority: 'high' }, + { id: 'J-1006', customer: 'Harbor View', service: 'Drain Cleaning', date: '2024-02-14', technician: 'Tom Wilson', status: 'scheduled', value: 175, priority: 'low' }, + { id: 'J-1007', customer: 'Pine Street House', service: 'Furnace Repair', date: '2024-02-14', technician: 'Mike Johnson', status: 'completed', value: 620, priority: 'high' }, + { id: 'J-1008', customer: 'Valley Office Park', service: 'Commercial HVAC', date: '2024-02-15', technician: 'Sarah Lee', status: 'scheduled', value: 2400, priority: 'high' }, + { id: 'J-1009', customer: 'Johnson Residence', service: 'Pipe Inspection', date: '2024-02-10', technician: 'Tom Wilson', status: 'completed', value: 220, priority: 'medium' }, + { id: 'J-1010', customer: 'City Hall', service: 'Electrical Upgrade', date: '2024-02-09', technician: 'Mike Johnson', status: 'cancelled', value: 0, priority: 'low' } +]; + +export default function JobGrid({ api }: any) { + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [priorityFilter, setPriorityFilter] = useState('all'); + + const filteredJobs = useMemo(() => { + return mockJobs.filter(job => { + const matchesSearch = searchTerm === '' || + job.customer.toLowerCase().includes(searchTerm.toLowerCase()) || + job.service.toLowerCase().includes(searchTerm.toLowerCase()) || + job.id.toLowerCase().includes(searchTerm.toLowerCase()) || + job.technician.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesStatus = statusFilter === 'all' || job.status === statusFilter; + const matchesPriority = priorityFilter === 'all' || job.priority === priorityFilter; + + return matchesSearch && matchesStatus && matchesPriority; + }); + }, [searchTerm, statusFilter, priorityFilter]); + + const getStatusClass = (status: string) => { + const classes: Record = { + 'open': 'status-open', + 'scheduled': 'status-scheduled', + 'in-progress': 'status-progress', + 'completed': 'status-completed', + 'cancelled': 'status-cancelled' + }; + return classes[status] || ''; + }; + + const getPriorityClass = (priority: string) => { + const classes: Record = { + 'low': 'priority-low', + 'medium': 'priority-medium', + 'high': 'priority-high' + }; + return classes[priority] || ''; + }; + + return ( +
+
+

All Jobs

+
+ setSearchTerm(e.target.value)} + className="search-input" + /> +
+
+ +
+
+ + +
+
+ + +
+
+ {filteredJobs.length} job{filteredJobs.length !== 1 ? 's' : ''} +
+
+ +
+ + + + + + + + + + + + + + + {filteredJobs.map(job => ( + + + + + + + + + + + ))} + +
Job IDCustomerServiceDateTechnicianStatusPriorityValue
{job.id}{job.customer}{job.service}{new Date(job.date).toLocaleDateString()}{job.technician} + + {job.status.replace('-', ' ')} + + + + {job.priority} + + ${job.value.toLocaleString()}
+
+
+ ); +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/job-grid/index.html b/servers/housecall-pro/src/ui/react-app/src/apps/job-grid/index.html new file mode 100644 index 0000000..f6a7e5a --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/job-grid/index.html @@ -0,0 +1,12 @@ + + + + + + Job Grid - Housecall Pro + + +
+ + + diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/job-grid/main.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/job-grid/main.tsx new file mode 100644 index 0000000..0255055 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/job-grid/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/job-grid/styles.css b/servers/housecall-pro/src/ui/react-app/src/apps/job-grid/styles.css new file mode 100644 index 0000000..d9ece2f --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/job-grid/styles.css @@ -0,0 +1,208 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + background: #0f172a; + color: #e2e8f0; + padding: 20px; +} + +.job-grid { + max-width: 1600px; + margin: 0 auto; +} + +.grid-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; +} + +.grid-header h1 { + font-size: 32px; + font-weight: 700; + color: #f1f5f9; +} + +.search-input { + width: 400px; + padding: 12px 16px; + background: #1e293b; + border: 1px solid #334155; + border-radius: 8px; + color: #e2e8f0; + font-size: 14px; +} + +.search-input:focus { + outline: none; + border-color: #3b82f6; +} + +.search-input::placeholder { + color: #64748b; +} + +.filters { + display: flex; + gap: 20px; + align-items: flex-end; + margin-bottom: 24px; + padding: 20px; + background: #1e293b; + border-radius: 12px; +} + +.filter-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.filter-group label { + font-size: 12px; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.filter-group select { + padding: 10px 14px; + background: #0f172a; + border: 1px solid #334155; + border-radius: 6px; + color: #e2e8f0; + font-size: 14px; + min-width: 180px; +} + +.filter-group select:focus { + outline: none; + border-color: #3b82f6; +} + +.results-count { + margin-left: auto; + padding: 10px 16px; + background: #0f172a; + border-radius: 6px; + color: #94a3b8; + font-size: 14px; +} + +.table-container { + background: #1e293b; + border-radius: 12px; + overflow: hidden; + border: 1px solid #334155; +} + +.jobs-table { + width: 100%; + border-collapse: collapse; +} + +.jobs-table thead { + background: #0f172a; +} + +.jobs-table th { + padding: 16px; + text-align: left; + font-size: 12px; + font-weight: 600; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 1px solid #334155; +} + +.jobs-table tbody tr { + border-bottom: 1px solid #334155; + transition: background 0.15s; +} + +.jobs-table tbody tr:hover { + background: rgba(59, 130, 246, 0.05); +} + +.jobs-table tbody tr:last-child { + border-bottom: none; +} + +.jobs-table td { + padding: 16px; + font-size: 14px; + color: #e2e8f0; +} + +.job-id { + color: #3b82f6; + font-weight: 600; +} + +.customer-name { + font-weight: 500; + color: #f1f5f9; +} + +.status-badge, +.priority-badge { + padding: 4px 12px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + text-transform: capitalize; + display: inline-block; +} + +.status-open { + background: rgba(59, 130, 246, 0.15); + color: #60a5fa; +} + +.status-scheduled { + background: rgba(139, 92, 246, 0.15); + color: #a78bfa; +} + +.status-progress { + background: rgba(245, 158, 11, 0.15); + color: #fbbf24; +} + +.status-completed { + background: rgba(16, 185, 129, 0.15); + color: #34d399; +} + +.status-cancelled { + background: rgba(239, 68, 68, 0.15); + color: #f87171; +} + +.priority-low { + background: rgba(148, 163, 184, 0.15); + color: #94a3b8; +} + +.priority-medium { + background: rgba(245, 158, 11, 0.15); + color: #fbbf24; +} + +.priority-high { + background: rgba(239, 68, 68, 0.15); + color: #f87171; +} + +.value { + font-weight: 600; + color: #10b981; + text-align: right; +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/payment-history/App.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/payment-history/App.tsx new file mode 100644 index 0000000..3206726 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/payment-history/App.tsx @@ -0,0 +1,181 @@ +/** + * Payment History - Payment log with filters by date, customer, method + */ + +import React, { useState, useMemo } from 'react'; +import './styles.css'; + +interface Payment { + id: string; + date: string; + customer: string; + jobId: string; + amount: number; + method: 'credit-card' | 'cash' | 'check' | 'ach' | 'other'; + status: 'completed' | 'pending' | 'failed' | 'refunded'; + invoice: string; +} + +const mockPayments: Payment[] = [ + { id: 'P-1001', date: '2024-02-12', customer: 'John Smith', jobId: 'J-1001', amount: 450, method: 'credit-card', status: 'completed', invoice: 'INV-5001' }, + { id: 'P-1002', date: '2024-02-12', customer: 'Sarah Johnson', jobId: 'J-1002', amount: 1200, method: 'ach', status: 'completed', invoice: 'INV-5002' }, + { id: 'P-1003', date: '2024-02-11', customer: 'Mike Williams', jobId: 'J-998', amount: 300, method: 'cash', status: 'completed', invoice: 'INV-4998' }, + { id: 'P-1004', date: '2024-02-11', customer: 'Emily Davis', jobId: 'J-997', amount: 850, method: 'credit-card', status: 'completed', invoice: 'INV-4997' }, + { id: 'P-1005', date: '2024-02-10', customer: 'Robert Martinez', jobId: 'J-995', amount: 2400, method: 'check', status: 'pending', invoice: 'INV-4995' }, + { id: 'P-1006', date: '2024-02-10', customer: 'Lisa Anderson', jobId: 'J-993', amount: 175, method: 'credit-card', status: 'completed', invoice: 'INV-4993' }, + { id: 'P-1007', date: '2024-02-09', customer: 'David Thompson', jobId: 'J-990', amount: 620, method: 'credit-card', status: 'completed', invoice: 'INV-4990' }, + { id: 'P-1008', date: '2024-02-09', customer: 'Jennifer Garcia', jobId: 'J-988', amount: 500, method: 'cash', status: 'completed', invoice: 'INV-4988' }, + { id: 'P-1009', date: '2024-02-08', customer: 'William Brown', jobId: 'J-985', amount: 1100, method: 'ach', status: 'failed', invoice: 'INV-4985' }, + { id: 'P-1010', date: '2024-02-08', customer: 'Amanda Wilson', jobId: 'J-983', amount: 380, method: 'credit-card', status: 'completed', invoice: 'INV-4983' }, + { id: 'P-1011', date: '2024-02-07', customer: 'James Lee', jobId: 'J-980', amount: 225, method: 'cash', status: 'completed', invoice: 'INV-4980' }, + { id: 'P-1012', date: '2024-02-07', customer: 'Maria Rodriguez', jobId: 'J-978', amount: 950, method: 'credit-card', status: 'refunded', invoice: 'INV-4978' }, + { id: 'P-1013', date: '2024-02-06', customer: 'Thomas Clark', jobId: 'J-975', amount: 1500, method: 'ach', status: 'completed', invoice: 'INV-4975' }, + { id: 'P-1014', date: '2024-02-06', customer: 'Patricia Miller', jobId: 'J-972', amount: 420, method: 'check', status: 'completed', invoice: 'INV-4972' }, + { id: 'P-1015', date: '2024-02-05', customer: 'Christopher White', jobId: 'J-970', amount: 680, method: 'credit-card', status: 'completed', invoice: 'INV-4970' } +]; + +export default function PaymentHistory({ api }: any) { + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [methodFilter, setMethodFilter] = useState('all'); + const [dateFilter, setDateFilter] = useState('all'); + + const filteredPayments = useMemo(() => { + return mockPayments.filter(payment => { + const matchesSearch = searchTerm === '' || + payment.customer.toLowerCase().includes(searchTerm.toLowerCase()) || + payment.id.toLowerCase().includes(searchTerm.toLowerCase()) || + payment.jobId.toLowerCase().includes(searchTerm.toLowerCase()) || + payment.invoice.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesStatus = statusFilter === 'all' || payment.status === statusFilter; + const matchesMethod = methodFilter === 'all' || payment.method === methodFilter; + + let matchesDate = true; + if (dateFilter !== 'all') { + const paymentDate = new Date(payment.date); + const today = new Date(); + const daysDiff = Math.floor((today.getTime() - paymentDate.getTime()) / (1000 * 60 * 60 * 24)); + + if (dateFilter === 'today') matchesDate = daysDiff === 0; + else if (dateFilter === 'week') matchesDate = daysDiff <= 7; + else if (dateFilter === 'month') matchesDate = daysDiff <= 30; + } + + return matchesSearch && matchesStatus && matchesMethod && matchesDate; + }); + }, [searchTerm, statusFilter, methodFilter, dateFilter]); + + const totalAmount = useMemo(() => { + return filteredPayments + .filter(p => p.status === 'completed') + .reduce((sum, p) => sum + p.amount, 0); + }, [filteredPayments]); + + const getStatusClass = (status: string) => { + const classes: Record = { + 'completed': 'status-completed', + 'pending': 'status-pending', + 'failed': 'status-failed', + 'refunded': 'status-refunded' + }; + return classes[status] || ''; + }; + + const getMethodIcon = (method: string) => { + const icons: Record = { + 'credit-card': '💳', + 'cash': '💵', + 'check': '📝', + 'ach': '🏦', + 'other': '💰' + }; + return icons[method] || '💰'; + }; + + return ( +
+
+

Payment History

+
+ Total (Filtered): ${totalAmount.toLocaleString()} +
+
+ +
+ setSearchTerm(e.target.value)} + className="search-input" + /> +
+ + + +
+
+ {filteredPayments.length} payment{filteredPayments.length !== 1 ? 's' : ''} +
+
+ +
+ + + + + + + + + + + + + + + {filteredPayments.map(payment => ( + + + + + + + + + + + ))} + +
Payment IDDateCustomerJob IDInvoiceMethodAmountStatus
{payment.id}{new Date(payment.date).toLocaleDateString()}{payment.customer}{payment.jobId}{payment.invoice} + + {getMethodIcon(payment.method)} {payment.method.replace('-', ' ')} + + ${payment.amount.toLocaleString()} + + {payment.status} + +
+
+
+ ); +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/payment-history/index.html b/servers/housecall-pro/src/ui/react-app/src/apps/payment-history/index.html new file mode 100644 index 0000000..b476226 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/payment-history/index.html @@ -0,0 +1,12 @@ + + + + + + Payment History - Housecall Pro + + +
+ + + diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/payment-history/main.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/payment-history/main.tsx new file mode 100644 index 0000000..0255055 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/payment-history/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/payment-history/styles.css b/servers/housecall-pro/src/ui/react-app/src/apps/payment-history/styles.css new file mode 100644 index 0000000..354192c --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/payment-history/styles.css @@ -0,0 +1,209 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + background: #0f172a; + color: #e2e8f0; + padding: 20px; +} + +.payment-history { + max-width: 1600px; + margin: 0 auto; +} + +.history-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; +} + +.history-header h1 { + font-size: 32px; + font-weight: 700; + color: #f1f5f9; +} + +.total-banner { + background: linear-gradient(135deg, #10b981, #059669); + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + color: white; +} + +.total-amount { + font-size: 24px; + font-weight: 700; + margin-left: 8px; +} + +.controls { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 24px; + padding: 20px; + background: #1e293b; + border-radius: 12px; + border: 1px solid #334155; +} + +.search-input { + width: 100%; + padding: 12px 16px; + background: #0f172a; + border: 1px solid #334155; + border-radius: 8px; + color: #e2e8f0; + font-size: 14px; +} + +.search-input:focus { + outline: none; + border-color: #3b82f6; +} + +.search-input::placeholder { + color: #64748b; +} + +.filters { + display: flex; + gap: 12px; +} + +.filters select { + flex: 1; + padding: 10px 14px; + background: #0f172a; + border: 1px solid #334155; + border-radius: 6px; + color: #e2e8f0; + font-size: 14px; +} + +.filters select:focus { + outline: none; + border-color: #3b82f6; +} + +.results-count { + text-align: right; + color: #94a3b8; + font-size: 14px; +} + +.table-container { + background: #1e293b; + border-radius: 12px; + overflow: hidden; + border: 1px solid #334155; +} + +.payments-table { + width: 100%; + border-collapse: collapse; +} + +.payments-table thead { + background: #0f172a; +} + +.payments-table th { + padding: 16px; + text-align: left; + font-size: 12px; + font-weight: 600; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 1px solid #334155; +} + +.payments-table tbody tr { + border-bottom: 1px solid #334155; + transition: background 0.15s; +} + +.payments-table tbody tr:hover { + background: rgba(59, 130, 246, 0.05); + cursor: pointer; +} + +.payments-table tbody tr:last-child { + border-bottom: none; +} + +.payments-table td { + padding: 16px; + font-size: 14px; + color: #e2e8f0; +} + +.payment-id { + color: #3b82f6; + font-weight: 600; +} + +.customer-name { + font-weight: 500; + color: #f1f5f9; +} + +.job-id, +.invoice-id { + color: #8b5cf6; + font-weight: 500; + font-size: 13px; +} + +.payment-method { + display: inline-flex; + align-items: center; + gap: 6px; + text-transform: capitalize; + color: #94a3b8; +} + +.amount { + font-weight: 700; + color: #10b981; + text-align: right; + font-size: 15px; +} + +.status-badge { + padding: 4px 12px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + text-transform: capitalize; + display: inline-block; +} + +.status-completed { + background: rgba(16, 185, 129, 0.15); + color: #34d399; +} + +.status-pending { + background: rgba(245, 158, 11, 0.15); + color: #fbbf24; +} + +.status-failed { + background: rgba(239, 68, 68, 0.15); + color: #f87171; +} + +.status-refunded { + background: rgba(139, 92, 246, 0.15); + color: #a78bfa; +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/App.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/App.tsx new file mode 100644 index 0000000..ecb65de --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/App.tsx @@ -0,0 +1,192 @@ +/** + * Revenue Dashboard - Revenue charts by period, service type, technician + */ + +import React, { useState } from 'react'; +import './styles.css'; + +interface RevenueData { + today: number; + week: number; + month: number; + year: number; +} + +interface ServiceRevenue { + service: string; + revenue: number; + jobs: number; + percentage: number; +} + +interface TechRevenue { + name: string; + revenue: number; + jobs: number; +} + +interface DailyRevenue { + date: string; + revenue: number; +} + +const revenueData: RevenueData = { + today: 3850, + week: 28450, + month: 124600, + year: 892350 +}; + +const serviceRevenue: ServiceRevenue[] = [ + { service: 'HVAC Repair', revenue: 45200, jobs: 82, percentage: 36.3 }, + { service: 'Plumbing', revenue: 32800, jobs: 104, percentage: 26.3 }, + { service: 'Electrical', revenue: 28400, jobs: 96, percentage: 22.8 }, + { service: 'Maintenance', revenue: 12600, jobs: 142, percentage: 10.1 }, + { service: 'Installation', revenue: 5600, jobs: 12, percentage: 4.5 } +]; + +const techRevenue: TechRevenue[] = [ + { name: 'Mike Johnson', revenue: 38450, jobs: 124 }, + { name: 'Sarah Lee', revenue: 42800, jobs: 136 }, + { name: 'Tom Wilson', revenue: 28900, jobs: 98 }, + { name: 'Jessica Martinez', revenue: 14450, jobs: 78 } +]; + +const dailyRevenue: DailyRevenue[] = [ + { date: '02/06', revenue: 18200 }, + { date: '02/07', revenue: 21500 }, + { date: '02/08', revenue: 16800 }, + { date: '02/09', revenue: 24300 }, + { date: '02/10', revenue: 19700 }, + { date: '02/11', revenue: 22900 }, + { date: '02/12', revenue: 20150 } +]; + +export default function RevenueDashboard({ api }: any) { + const [period, setPeriod] = useState<'today' | 'week' | 'month' | 'year'>('month'); + + const maxDailyRevenue = Math.max(...dailyRevenue.map(d => d.revenue)); + + return ( +
+
+

Revenue Dashboard

+
+ + + + +
+
+ +
+
+
Today
+
${revenueData.today.toLocaleString()}
+
+12.5%
+
+
+
This Week
+
${revenueData.week.toLocaleString()}
+
+8.3%
+
+
+
This Month
+
${revenueData.month.toLocaleString()}
+
+15.7%
+
+
+
This Year
+
${revenueData.year.toLocaleString()}
+
+22.1%
+
+
+ +
+
+

Daily Revenue (Last 7 Days)

+
+ {dailyRevenue.map(day => ( +
+
+
+
${(day.revenue / 1000).toFixed(1)}k
+
+
+
{day.date}
+
+ ))} +
+
+ +
+

Revenue by Service Type

+
+ {serviceRevenue.map((service, idx) => ( +
+
+
{service.service}
+
+ {service.jobs} jobs +
+
+
+
+
+
+ ${service.revenue.toLocaleString()} +
+
+ ))} +
+
+
+ +
+

Technician Performance

+
+ {techRevenue.map((tech, idx) => ( +
+
+ {tech.name.split(' ').map(n => n[0]).join('')} +
+
+
{tech.name}
+
+
${tech.revenue.toLocaleString()}
+
{tech.jobs} jobs
+
+
+
+ ))} +
+
+
+ ); +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/index.html b/servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/index.html new file mode 100644 index 0000000..4281bf5 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Revenue Dashboard - Housecall Pro + + +
+ + + diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/main.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/main.tsx new file mode 100644 index 0000000..0255055 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/styles.css b/servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/styles.css new file mode 100644 index 0000000..b0e9448 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/revenue-dashboard/styles.css @@ -0,0 +1,310 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + background: #0f172a; + color: #e2e8f0; + padding: 20px; +} + +.revenue-dashboard { + max-width: 1400px; + margin: 0 auto; +} + +.dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 32px; +} + +.dashboard-header h1 { + font-size: 32px; + font-weight: 700; + color: #f1f5f9; +} + +.period-selector { + display: flex; + gap: 8px; + background: #1e293b; + padding: 4px; + border-radius: 8px; +} + +.period-selector button { + padding: 8px 20px; + background: transparent; + border: none; + border-radius: 6px; + color: #94a3b8; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.period-selector button:hover { + color: #e2e8f0; +} + +.period-selector button.active { + background: #3b82f6; + color: white; +} + +.revenue-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 32px; +} + +.stat-card { + background: #1e293b; + padding: 24px; + border-radius: 12px; + border-left: 4px solid #3b82f6; +} + +.stat-card.stat-today { border-left-color: #10b981; } +.stat-card.stat-week { border-left-color: #3b82f6; } +.stat-card.stat-month { border-left-color: #8b5cf6; } +.stat-card.stat-year { border-left-color: #f59e0b; } + +.stat-label { + font-size: 13px; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 8px; +} + +.stat-value { + font-size: 32px; + font-weight: 700; + color: #f1f5f9; + margin-bottom: 8px; +} + +.stat-change { + font-size: 14px; + font-weight: 600; +} + +.stat-change.positive { + color: #10b981; +} + +.stat-change.negative { + color: #ef4444; +} + +.charts-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + margin-bottom: 32px; +} + +.chart-card { + background: #1e293b; + padding: 24px; + border-radius: 12px; + border: 1px solid #334155; +} + +.chart-card h2 { + font-size: 18px; + color: #f1f5f9; + margin-bottom: 24px; +} + +.bar-chart { + display: flex; + gap: 12px; + align-items: flex-end; + height: 240px; + padding-top: 20px; +} + +.bar-group { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.bar-container { + width: 100%; + height: 200px; + display: flex; + align-items: flex-end; + justify-content: center; +} + +.bar { + width: 100%; + background: linear-gradient(180deg, #3b82f6, #1e40af); + border-radius: 6px 6px 0 0; + position: relative; + min-height: 20px; + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 8px; + transition: all 0.3s; +} + +.bar:hover { + opacity: 0.8; +} + +.bar-value { + font-size: 11px; + font-weight: 600; + color: white; +} + +.bar-label { + font-size: 12px; + color: #94a3b8; +} + +.service-breakdown { + display: flex; + flex-direction: column; + gap: 16px; +} + +.service-row { + display: grid; + grid-template-columns: 200px 1fr 120px; + gap: 16px; + align-items: center; +} + +.service-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.service-name { + font-weight: 600; + color: #f1f5f9; + font-size: 14px; +} + +.service-meta { + font-size: 12px; + color: #64748b; +} + +.service-bar-container { + background: #0f172a; + height: 32px; + border-radius: 6px; + overflow: hidden; +} + +.service-bar { + height: 100%; + background: linear-gradient(90deg, #8b5cf6, #6d28d9); + border-radius: 6px; + transition: width 0.5s ease; +} + +.service-revenue { + font-weight: 700; + color: #10b981; + text-align: right; + font-size: 16px; +} + +.tech-performance { + background: #1e293b; + padding: 24px; + border-radius: 12px; + border: 1px solid #334155; +} + +.tech-performance h2 { + font-size: 20px; + color: #f1f5f9; + margin-bottom: 24px; +} + +.tech-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; +} + +.tech-card { + display: flex; + gap: 16px; + padding: 16px; + background: #0f172a; + border-radius: 8px; + border: 1px solid #334155; + transition: all 0.2s; +} + +.tech-card:hover { + border-color: #3b82f6; + transform: translateY(-2px); +} + +.tech-avatar { + width: 56px; + height: 56px; + border-radius: 50%; + background: linear-gradient(135deg, #3b82f6, #8b5cf6); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + font-weight: 700; + color: white; + flex-shrink: 0; +} + +.tech-info { + flex: 1; +} + +.tech-name { + font-weight: 600; + color: #f1f5f9; + margin-bottom: 8px; + font-size: 15px; +} + +.tech-stats { + display: flex; + gap: 16px; + align-items: baseline; +} + +.tech-revenue { + font-size: 20px; + font-weight: 700; + color: #10b981; +} + +.tech-jobs { + font-size: 13px; + color: #64748b; +} + +@media (max-width: 1024px) { + .charts-grid { + grid-template-columns: 1fr; + } +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/App.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/App.tsx new file mode 100644 index 0000000..f35af83 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/App.tsx @@ -0,0 +1,198 @@ +/** + * Review Tracker - Customer reviews with ratings, response status + */ + +import React, { useState, useMemo } from 'react'; +import './styles.css'; + +interface Review { + id: string; + customer: string; + technician: string; + jobId: string; + rating: number; + date: string; + comment: string; + response?: string; + responseDate?: string; + status: 'pending' | 'responded' | 'flagged'; + source: 'google' | 'yelp' | 'facebook' | 'internal'; +} + +const mockReviews: Review[] = [ + { id: 'R-1001', customer: 'Sarah Wilson', technician: 'Mike Johnson', jobId: 'J-998', rating: 5, date: '2024-02-12', comment: 'Mike was professional and fixed our AC quickly! Highly recommend.', status: 'pending', source: 'google' }, + { id: 'R-1002', customer: 'John Davis', technician: 'Sarah Lee', jobId: 'J-995', rating: 5, date: '2024-02-11', comment: 'Excellent service, very knowledgeable. Will definitely use again.', response: 'Thank you for your kind words! We appreciate your business.', responseDate: '2024-02-11', status: 'responded', source: 'yelp' }, + { id: 'R-1003', customer: 'Emily Chen', technician: 'Tom Wilson', jobId: 'J-992', rating: 4, date: '2024-02-11', comment: 'Good work, arrived on time. Would have been 5 stars if the price was a bit lower.', status: 'pending', source: 'google' }, + { id: 'R-1004', customer: 'Robert Martinez', technician: 'Mike Johnson', jobId: 'J-989', rating: 5, date: '2024-02-10', comment: 'Outstanding! Will request Mike again for all future work.', response: 'We are thrilled to hear about your experience! Mike will be happy to assist you again.', responseDate: '2024-02-10', status: 'responded', source: 'facebook' }, + { id: 'R-1005', customer: 'Lisa Taylor', technician: 'Sarah Lee', jobId: 'J-985', rating: 2, date: '2024-02-09', comment: 'Technician was late and the job took longer than estimated. Not happy with the experience.', status: 'flagged', source: 'google' }, + { id: 'R-1006', customer: 'David Brown', technician: 'Tom Wilson', jobId: 'J-982', rating: 5, date: '2024-02-09', comment: 'Perfect service from start to finish. Very satisfied!', response: 'Thank you for choosing us! We look forward to serving you again.', responseDate: '2024-02-09', status: 'responded', source: 'internal' }, + { id: 'R-1007', customer: 'Jennifer White', technician: 'Mike Johnson', jobId: 'J-978', rating: 4, date: '2024-02-08', comment: 'Great technician, minor issue with scheduling but overall good experience.', status: 'pending', source: 'yelp' }, + { id: 'R-1008', customer: 'Michael Garcia', technician: 'Sarah Lee', jobId: 'J-975', rating: 5, date: '2024-02-07', comment: 'Sarah went above and beyond. Explained everything clearly.', response: 'We appreciate your feedback! Sarah takes pride in her work.', responseDate: '2024-02-07', status: 'responded', source: 'google' }, + { id: 'R-1009', customer: 'Amanda Clark', technician: 'Tom Wilson', jobId: 'J-970', rating: 3, date: '2024-02-06', comment: 'Service was okay, nothing special. Expected more based on reviews.', status: 'pending', source: 'facebook' }, + { id: 'R-1010', customer: 'Christopher Lee', technician: 'Mike Johnson', jobId: 'J-968', rating: 5, date: '2024-02-05', comment: 'Fantastic work! Mike is a true professional.', response: 'Thank you! We are glad Mike could help you.', responseDate: '2024-02-06', status: 'responded', source: 'google' } +]; + +export default function ReviewTracker({ api }: any) { + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [ratingFilter, setRatingFilter] = useState('all'); + const [sourceFilter, setSourceFilter] = useState('all'); + + const filteredReviews = useMemo(() => { + return mockReviews.filter(review => { + const matchesSearch = searchTerm === '' || + review.customer.toLowerCase().includes(searchTerm.toLowerCase()) || + review.technician.toLowerCase().includes(searchTerm.toLowerCase()) || + review.comment.toLowerCase().includes(searchTerm.toLowerCase()) || + review.id.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesStatus = statusFilter === 'all' || review.status === statusFilter; + const matchesRating = ratingFilter === 'all' || review.rating === parseInt(ratingFilter); + const matchesSource = sourceFilter === 'all' || review.source === sourceFilter; + + return matchesSearch && matchesStatus && matchesRating && matchesSource; + }); + }, [searchTerm, statusFilter, ratingFilter, sourceFilter]); + + const averageRating = useMemo(() => { + if (filteredReviews.length === 0) return 0; + return (filteredReviews.reduce((sum, r) => sum + r.rating, 0) / filteredReviews.length).toFixed(1); + }, [filteredReviews]); + + const getStatusClass = (status: string) => { + const classes: Record = { + 'pending': 'status-pending', + 'responded': 'status-responded', + 'flagged': 'status-flagged' + }; + return classes[status] || ''; + }; + + const getSourceClass = (source: string) => { + const classes: Record = { + 'google': 'source-google', + 'yelp': 'source-yelp', + 'facebook': 'source-facebook', + 'internal': 'source-internal' + }; + return classes[source] || ''; + }; + + const renderStars = (rating: number) => { + return Array.from({ length: 5 }, (_, i) => ( + + )); + }; + + return ( +
+
+
+

Review Tracker

+
+ + ⭐ {averageRating} Average Rating + + + 📝 {filteredReviews.length} Reviews + + + ⏳ {filteredReviews.filter(r => r.status === 'pending').length} Pending Response + +
+
+
+ +
+ setSearchTerm(e.target.value)} + className="search-input" + /> +
+ + + +
+
+ +
+ {filteredReviews.map(review => ( +
+
+
+
{review.customer}
+
{renderStars(review.rating)}
+
+
+ + {review.source} + + + {review.status} + +
+
+ +
+ {review.id} + + Technician: {review.technician} + + Job: {review.jobId} + + {new Date(review.date).toLocaleDateString()} +
+ +
+ {review.comment} +
+ + {review.response && ( +
+
+ Response + {review.responseDate && ( + + {new Date(review.responseDate).toLocaleDateString()} + + )} +
+
{review.response}
+
+ )} + + {!review.response && review.status === 'pending' && ( +
+ + {review.rating <= 3 && ( + + )} +
+ )} +
+ ))} +
+
+ ); +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/index.html b/servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/index.html new file mode 100644 index 0000000..4a5fcf9 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/index.html @@ -0,0 +1,12 @@ + + + + + + Review Tracker - Housecall Pro + + +
+ + + diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/main.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/main.tsx new file mode 100644 index 0000000..0255055 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/styles.css b/servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/styles.css new file mode 100644 index 0000000..65b3e42 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/review-tracker/styles.css @@ -0,0 +1,296 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + background: #0f172a; + color: #e2e8f0; + padding: 20px; +} + +.review-tracker { + max-width: 1200px; + margin: 0 auto; +} + +.tracker-header { + margin-bottom: 32px; +} + +.tracker-header h1 { + font-size: 32px; + font-weight: 700; + color: #f1f5f9; + margin-bottom: 12px; +} + +.header-stats { + display: flex; + gap: 24px; + color: #94a3b8; + font-size: 14px; +} + +.stat-item { + display: flex; + align-items: center; + gap: 6px; +} + +.controls { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 24px; + padding: 20px; + background: #1e293b; + border-radius: 12px; + border: 1px solid #334155; +} + +.search-input { + width: 100%; + padding: 12px 16px; + background: #0f172a; + border: 1px solid #334155; + border-radius: 8px; + color: #e2e8f0; + font-size: 14px; +} + +.search-input:focus { + outline: none; + border-color: #3b82f6; +} + +.search-input::placeholder { + color: #64748b; +} + +.filters { + display: flex; + gap: 12px; +} + +.filters select { + flex: 1; + padding: 10px 14px; + background: #0f172a; + border: 1px solid #334155; + border-radius: 6px; + color: #e2e8f0; + font-size: 14px; +} + +.filters select:focus { + outline: none; + border-color: #3b82f6; +} + +.reviews-container { + display: flex; + flex-direction: column; + gap: 16px; +} + +.review-card { + background: #1e293b; + padding: 24px; + border-radius: 12px; + border: 1px solid #334155; + border-left: 4px solid #3b82f6; +} + +.review-card.status-pending { + border-left-color: #f59e0b; +} + +.review-card.status-responded { + border-left-color: #10b981; +} + +.review-card.status-flagged { + border-left-color: #ef4444; + background: rgba(239, 68, 68, 0.05); +} + +.review-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +.review-meta { + display: flex; + flex-direction: column; + gap: 8px; +} + +.customer-name { + font-size: 18px; + font-weight: 600; + color: #f1f5f9; +} + +.review-rating { + display: flex; + gap: 2px; +} + +.star { + color: #334155; + font-size: 18px; +} + +.star.filled { + color: #fbbf24; +} + +.review-badges { + display: flex; + gap: 8px; +} + +.source-badge, +.status-badge { + padding: 4px 12px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + text-transform: capitalize; +} + +.source-google { + background: rgba(234, 67, 53, 0.15); + color: #f87171; +} + +.source-yelp { + background: rgba(239, 68, 68, 0.15); + color: #f87171; +} + +.source-facebook { + background: rgba(59, 130, 246, 0.15); + color: #60a5fa; +} + +.source-internal { + background: rgba(139, 92, 246, 0.15); + color: #a78bfa; +} + +.status-pending { + background: rgba(245, 158, 11, 0.15); + color: #fbbf24; +} + +.status-responded { + background: rgba(16, 185, 129, 0.15); + color: #34d399; +} + +.status-flagged { + background: rgba(239, 68, 68, 0.15); + color: #f87171; +} + +.review-info { + display: flex; + gap: 8px; + align-items: center; + font-size: 13px; + color: #64748b; + margin-bottom: 16px; +} + +.review-id { + color: #3b82f6; + font-weight: 600; +} + +.separator { + color: #334155; +} + +.review-comment { + font-size: 15px; + line-height: 1.6; + color: #e2e8f0; + margin-bottom: 16px; +} + +.review-response { + background: #0f172a; + padding: 16px; + border-radius: 8px; + border-left: 3px solid #10b981; + margin-top: 16px; +} + +.response-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.response-label { + font-size: 12px; + font-weight: 600; + color: #10b981; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.response-date { + font-size: 12px; + color: #64748b; +} + +.response-text { + font-size: 14px; + line-height: 1.6; + color: #94a3b8; +} + +.review-actions { + display: flex; + gap: 12px; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #334155; +} + +.review-actions button { + padding: 8px 16px; + border-radius: 6px; + font-weight: 500; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; + border: none; +} + +.btn-respond { + background: #3b82f6; + color: white; +} + +.btn-respond:hover { + background: #2563eb; +} + +.btn-flag { + background: rgba(239, 68, 68, 0.1); + color: #f87171; + border: 1px solid #ef4444; +} + +.btn-flag:hover { + background: rgba(239, 68, 68, 0.2); +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/App.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/App.tsx new file mode 100644 index 0000000..eec8bc3 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/App.tsx @@ -0,0 +1,177 @@ +/** + * Technician Dashboard - Individual tech's schedule, performance, reviews + */ + +import React, { useState } from 'react'; +import './styles.css'; + +interface TechnicianData { + id: string; + name: string; + status: 'available' | 'on-job' | 'offline'; + rating: number; + totalReviews: number; + jobsCompleted: number; + revenueThisMonth: number; + hoursWorked: number; +} + +interface TodayJob { + id: string; + customer: string; + service: string; + time: string; + status: 'upcoming' | 'in-progress' | 'completed'; + value: number; + address: string; +} + +interface Review { + id: string; + customer: string; + rating: number; + comment: string; + date: string; + jobId: string; +} + +const mockTechnician: TechnicianData = { + id: 'T-001', + name: 'Mike Johnson', + status: 'on-job', + rating: 4.8, + totalReviews: 142, + jobsCompleted: 28, + revenueThisMonth: 18750, + hoursWorked: 156 +}; + +const todayJobs: TodayJob[] = [ + { id: 'J-1001', customer: 'Smith Residence', service: 'HVAC Repair', time: '09:00 AM', status: 'completed', value: 450, address: '123 Main St' }, + { id: 'J-1002', customer: 'Downtown Office', service: 'Plumbing Install', time: '11:30 AM', status: 'in-progress', value: 1200, address: '456 Oak Ave' }, + { id: 'J-1003', customer: 'Oak Street Apt', service: 'Electrical Inspection', time: '02:00 PM', status: 'upcoming', value: 200, address: '789 Pine Dr' }, + { id: 'J-1004', customer: 'Martinez Home', service: 'AC Maintenance', time: '04:30 PM', status: 'upcoming', value: 150, address: '321 Elm St' } +]; + +const recentReviews: Review[] = [ + { id: 'R-1', customer: 'Sarah Wilson', rating: 5, comment: 'Mike was professional and fixed our AC quickly!', date: '2024-02-11', jobId: 'J-998' }, + { id: 'R-2', customer: 'John Davis', rating: 5, comment: 'Excellent service, very knowledgeable.', date: '2024-02-10', jobId: 'J-995' }, + { id: 'R-3', customer: 'Emily Chen', rating: 4, comment: 'Good work, arrived on time.', date: '2024-02-09', jobId: 'J-992' }, + { id: 'R-4', customer: 'Robert Martinez', rating: 5, comment: 'Outstanding! Will request Mike again.', date: '2024-02-08', jobId: 'J-989' } +]; + +export default function TechnicianDashboard({ api }: any) { + const [tech] = useState(mockTechnician); + const [jobs] = useState(todayJobs); + const [reviews] = useState(recentReviews); + + const getStatusClass = (status: string) => { + const classes: Record = { + 'available': 'status-available', + 'on-job': 'status-on-job', + 'offline': 'status-offline', + 'upcoming': 'status-upcoming', + 'in-progress': 'status-in-progress', + 'completed': 'status-completed' + }; + return classes[status] || ''; + }; + + const renderStars = (rating: number) => { + return Array.from({ length: 5 }, (_, i) => ( + + )); + }; + + return ( +
+
+
+
{tech.name.split(' ').map(n => n[0]).join('')}
+
+

{tech.name}

+
+ + {tech.status.replace('-', ' ')} + + + ★ {tech.rating} ({tech.totalReviews} reviews) + +
+
+
+
+ +
+
+
+
+
{tech.jobsCompleted}
+
Jobs Completed (MTD)
+
+
+
+
💰
+
+
${tech.revenueThisMonth.toLocaleString()}
+
Revenue (MTD)
+
+
+
+
+
+
{tech.hoursWorked}h
+
Hours Worked (MTD)
+
+
+
+
+
+
{tech.rating}
+
Average Rating
+
+
+
+ +
+
+

Today's Schedule

+
+ {jobs.map(job => ( +
+
{job.time}
+
+
+ {job.id} + +
+
{job.customer}
+
{job.service}
+
📍 {job.address}
+
${job.value}
+
+
+ ))} +
+
+ +
+

Recent Reviews

+
+ {reviews.map(review => ( +
+
+
{review.customer}
+
{new Date(review.date).toLocaleDateString()}
+
+
{renderStars(review.rating)}
+
{review.comment}
+
Job #{review.jobId}
+
+ ))} +
+
+
+
+ ); +} diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/index.html b/servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/index.html new file mode 100644 index 0000000..cc956b3 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Technician Dashboard - Housecall Pro + + +
+ + + diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/main.tsx b/servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/main.tsx new file mode 100644 index 0000000..0255055 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/styles.css b/servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/styles.css new file mode 100644 index 0000000..46f9de4 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/src/apps/technician-dashboard/styles.css @@ -0,0 +1,308 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + background: #0f172a; + color: #e2e8f0; + padding: 20px; +} + +.technician-dashboard { + max-width: 1400px; + margin: 0 auto; +} + +.tech-header { + margin-bottom: 32px; + background: #1e293b; + padding: 24px; + border-radius: 12px; + border: 1px solid #334155; +} + +.tech-profile { + display: flex; + align-items: center; + gap: 20px; +} + +.avatar { + width: 80px; + height: 80px; + border-radius: 50%; + background: linear-gradient(135deg, #3b82f6, #8b5cf6); + display: flex; + align-items: center; + justify-content: center; + font-size: 32px; + font-weight: 700; + color: white; +} + +.tech-info h1 { + font-size: 28px; + font-weight: 700; + color: #f1f5f9; + margin-bottom: 8px; +} + +.tech-meta { + display: flex; + gap: 16px; + align-items: center; +} + +.status-badge { + padding: 6px 14px; + border-radius: 14px; + font-size: 12px; + font-weight: 600; + text-transform: capitalize; +} + +.status-available { + background: rgba(16, 185, 129, 0.15); + color: #34d399; +} + +.status-on-job { + background: rgba(245, 158, 11, 0.15); + color: #fbbf24; +} + +.status-offline { + background: rgba(100, 116, 139, 0.15); + color: #94a3b8; +} + +.rating { + color: #fbbf24; + font-size: 14px; + font-weight: 600; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 32px; +} + +.stat-card { + background: #1e293b; + padding: 24px; + border-radius: 12px; + border: 1px solid #334155; + display: flex; + gap: 16px; + align-items: center; +} + +.stat-icon { + font-size: 32px; + width: 56px; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(59, 130, 246, 0.1); + border-radius: 12px; +} + +.stat-content { + flex: 1; +} + +.stat-value { + font-size: 28px; + font-weight: 700; + color: #f1f5f9; + margin-bottom: 4px; +} + +.stat-label { + font-size: 13px; + color: #94a3b8; +} + +.dashboard-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; +} + +.schedule-section, +.reviews-section { + background: #1e293b; + padding: 24px; + border-radius: 12px; + border: 1px solid #334155; +} + +.schedule-section h2, +.reviews-section h2 { + font-size: 20px; + color: #f1f5f9; + margin-bottom: 20px; +} + +.jobs-timeline { + display: flex; + flex-direction: column; + gap: 16px; +} + +.timeline-job { + display: flex; + gap: 16px; + padding: 16px; + background: #0f172a; + border-radius: 8px; + border-left: 4px solid #3b82f6; +} + +.timeline-job.status-completed { + border-left-color: #10b981; + opacity: 0.7; +} + +.timeline-job.status-in-progress { + border-left-color: #f59e0b; +} + +.timeline-job.status-upcoming { + border-left-color: #8b5cf6; +} + +.job-time { + font-weight: 600; + color: #94a3b8; + min-width: 80px; + font-size: 14px; +} + +.job-details { + flex: 1; +} + +.job-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.job-id { + font-size: 12px; + font-weight: 600; + color: #3b82f6; +} + +.status-dot { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.status-dot.status-completed { + background: #10b981; +} + +.status-dot.status-in-progress { + background: #f59e0b; +} + +.status-dot.status-upcoming { + background: #8b5cf6; +} + +.job-customer { + font-size: 16px; + font-weight: 600; + color: #f1f5f9; + margin-bottom: 4px; +} + +.job-service { + font-size: 14px; + color: #94a3b8; + margin-bottom: 8px; +} + +.job-address { + font-size: 13px; + color: #64748b; + margin-bottom: 8px; +} + +.job-value { + font-weight: 600; + color: #10b981; + font-size: 15px; +} + +.reviews-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.review-card { + padding: 16px; + background: #0f172a; + border-radius: 8px; + border: 1px solid #334155; +} + +.review-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.review-customer { + font-weight: 600; + color: #f1f5f9; + font-size: 15px; +} + +.review-date { + font-size: 12px; + color: #64748b; +} + +.review-rating { + margin-bottom: 10px; +} + +.star { + color: #334155; + font-size: 18px; +} + +.star.filled { + color: #fbbf24; +} + +.review-comment { + color: #94a3b8; + font-size: 14px; + line-height: 1.6; + margin-bottom: 8px; +} + +.review-job { + font-size: 12px; + color: #3b82f6; +} + +@media (max-width: 1024px) { + .dashboard-content { + grid-template-columns: 1fr; + } +}