pacatui 0.1.5 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -3
- package/src/App.tsx +226 -8
- package/src/components/CreateInvoiceModal.tsx +27 -21
- package/src/components/CustomerModal.tsx +11 -7
- package/src/components/CustomerSelectModal.tsx +15 -12
- package/src/components/Dashboard.tsx +63 -72
- package/src/components/DateTimePicker.tsx +8 -8
- package/src/components/EditTimeEntryModal.tsx +16 -13
- package/src/components/Header.tsx +8 -4
- package/src/components/HelpView.tsx +14 -6
- package/src/components/InputModal.tsx +22 -11
- package/src/components/InvoicesView.tsx +33 -29
- package/src/components/Modal.tsx +12 -7
- package/src/components/ProjectList.tsx +17 -14
- package/src/components/ProjectModal.tsx +9 -5
- package/src/components/SettingsView.tsx +32 -10
- package/src/components/StatusBar.tsx +47 -8
- package/src/components/TaskList.tsx +24 -21
- package/src/components/ThemeSelectModal.tsx +94 -0
- package/src/components/Timer.tsx +14 -12
- package/src/components/TimerModals.tsx +19 -13
- package/src/components/TimesheetView.tsx +194 -15
- package/src/components/index.ts +1 -0
- package/src/db.ts +37 -0
- package/src/index.tsx +196 -129
- package/src/types.ts +311 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pacatui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "A simple tui app for task, timer and invoicing for projects.",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"type": "module",
|
|
@@ -66,8 +66,6 @@
|
|
|
66
66
|
"@prisma/adapter-libsql": "^7.2.0",
|
|
67
67
|
"@prisma/client": "^7.2.0",
|
|
68
68
|
"better-sqlite3": "^12.6.0",
|
|
69
|
-
"paca": "^1.0.10",
|
|
70
|
-
"pacatui": "^0.1.1",
|
|
71
69
|
"prisma": "^7.2.0",
|
|
72
70
|
"react": "^19.2.3",
|
|
73
71
|
"stripe": "^20.2.0"
|
package/src/App.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
CustomerSelectModal,
|
|
20
20
|
EditTimeEntryModal,
|
|
21
21
|
CreateInvoiceModal,
|
|
22
|
+
ThemeSelectModal,
|
|
22
23
|
} from "./components/index.ts";
|
|
23
24
|
import { InvoicesView } from "./components/InvoicesView.tsx";
|
|
24
25
|
import {
|
|
@@ -34,6 +35,8 @@ import {
|
|
|
34
35
|
import { getOrCreateStripeCustomer, createDraftInvoice, listInvoices, clearInvoiceCache, type StripeInvoiceItem } from "./stripe.ts";
|
|
35
36
|
import {
|
|
36
37
|
getEffectiveTimezone,
|
|
38
|
+
formatDateInTimezone,
|
|
39
|
+
getTheme,
|
|
37
40
|
type View,
|
|
38
41
|
type Panel,
|
|
39
42
|
type InputMode,
|
|
@@ -47,6 +50,7 @@ import {
|
|
|
47
50
|
type TimeEntry,
|
|
48
51
|
type TimeEntryWithProject,
|
|
49
52
|
type WeeklyTimeData,
|
|
53
|
+
type AllTimersWeekData,
|
|
50
54
|
} from "./types.ts";
|
|
51
55
|
|
|
52
56
|
function formatDuration(ms: number): string {
|
|
@@ -104,6 +108,7 @@ export function App() {
|
|
|
104
108
|
businessName: "",
|
|
105
109
|
stripeApiKey: "",
|
|
106
110
|
timezone: "auto",
|
|
111
|
+
theme: "catppuccin-mocha",
|
|
107
112
|
});
|
|
108
113
|
|
|
109
114
|
// Timesheet State
|
|
@@ -126,6 +131,13 @@ export function App() {
|
|
|
126
131
|
| null
|
|
127
132
|
>(null);
|
|
128
133
|
|
|
134
|
+
// All Timers State (for viewing all timers paged by week)
|
|
135
|
+
const [showAllTimers, setShowAllTimers] = useState(false);
|
|
136
|
+
const [allTimersWeekData, setAllTimersWeekData] = useState<AllTimersWeekData | null>(null);
|
|
137
|
+
const [allTimersSelectedIndex, setAllTimersSelectedIndex] = useState(0);
|
|
138
|
+
const [hasOlderWeeks, setHasOlderWeeks] = useState(false);
|
|
139
|
+
const [hasNewerWeeks, setHasNewerWeeks] = useState(false);
|
|
140
|
+
|
|
129
141
|
// Customer State
|
|
130
142
|
const [customerList, setCustomerList] = useState<Customer[]>([]);
|
|
131
143
|
const [editingCustomer, setEditingCustomer] = useState<Customer | null>(null);
|
|
@@ -293,6 +305,56 @@ export function App() {
|
|
|
293
305
|
}
|
|
294
306
|
}, [selectedTimesheetGroupIndex, selectedTimeEntryIndex]);
|
|
295
307
|
|
|
308
|
+
// Helper function to get the start of a week (Sunday)
|
|
309
|
+
const getWeekStart = useCallback((date: Date): Date => {
|
|
310
|
+
const d = new Date(date);
|
|
311
|
+
d.setHours(0, 0, 0, 0);
|
|
312
|
+
d.setDate(d.getDate() - d.getDay());
|
|
313
|
+
return d;
|
|
314
|
+
}, []);
|
|
315
|
+
|
|
316
|
+
// Load all timers for a specific week
|
|
317
|
+
const loadAllTimers = useCallback(async (weekStart?: Date) => {
|
|
318
|
+
const currentWeekStart = weekStart ?? getWeekStart(new Date());
|
|
319
|
+
const weekEnd = new Date(currentWeekStart);
|
|
320
|
+
weekEnd.setDate(weekEnd.getDate() + 7);
|
|
321
|
+
|
|
322
|
+
const entries = await timeEntries.getAllForWeek(currentWeekStart);
|
|
323
|
+
|
|
324
|
+
// Calculate total duration
|
|
325
|
+
let totalMs = 0;
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
if (entry.endTime) {
|
|
328
|
+
totalMs += new Date(entry.endTime).getTime() - new Date(entry.startTime).getTime();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
setAllTimersWeekData({
|
|
333
|
+
weekStart: currentWeekStart,
|
|
334
|
+
weekEnd,
|
|
335
|
+
entries: entries as TimeEntryWithProject[],
|
|
336
|
+
totalMs,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Check if there are older weeks
|
|
340
|
+
const oldestDate = await timeEntries.getOldestEntryDate();
|
|
341
|
+
if (oldestDate) {
|
|
342
|
+
const oldestWeekStart = getWeekStart(new Date(oldestDate));
|
|
343
|
+
setHasOlderWeeks(oldestWeekStart < currentWeekStart);
|
|
344
|
+
} else {
|
|
345
|
+
setHasOlderWeeks(false);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Check if there are newer weeks (current week start is before today's week start)
|
|
349
|
+
const todayWeekStart = getWeekStart(new Date());
|
|
350
|
+
setHasNewerWeeks(currentWeekStart < todayWeekStart);
|
|
351
|
+
|
|
352
|
+
// Adjust selection if needed
|
|
353
|
+
if (allTimersSelectedIndex >= entries.length) {
|
|
354
|
+
setAllTimersSelectedIndex(Math.max(0, entries.length - 1));
|
|
355
|
+
}
|
|
356
|
+
}, [getWeekStart, allTimersSelectedIndex]);
|
|
357
|
+
|
|
296
358
|
// Initial load
|
|
297
359
|
useEffect(() => {
|
|
298
360
|
loadProjects();
|
|
@@ -317,9 +379,13 @@ export function App() {
|
|
|
317
379
|
// Reload timesheets when on timesheets view
|
|
318
380
|
useEffect(() => {
|
|
319
381
|
if (currentView === "timesheets") {
|
|
320
|
-
|
|
382
|
+
if (showAllTimers) {
|
|
383
|
+
loadAllTimers(allTimersWeekData?.weekStart);
|
|
384
|
+
} else {
|
|
385
|
+
loadTimesheets();
|
|
386
|
+
}
|
|
321
387
|
}
|
|
322
|
-
}, [currentView]);
|
|
388
|
+
}, [currentView, showAllTimers]);
|
|
323
389
|
|
|
324
390
|
// Load invoices when on invoices view
|
|
325
391
|
useEffect(() => {
|
|
@@ -384,7 +450,11 @@ export function App() {
|
|
|
384
450
|
|
|
385
451
|
// Refresh timesheets if currently viewing them
|
|
386
452
|
if (currentView === "timesheets") {
|
|
387
|
-
|
|
453
|
+
if (showAllTimers) {
|
|
454
|
+
loadAllTimers(allTimersWeekData?.weekStart);
|
|
455
|
+
} else {
|
|
456
|
+
loadTimesheets();
|
|
457
|
+
}
|
|
388
458
|
}
|
|
389
459
|
};
|
|
390
460
|
|
|
@@ -784,13 +854,16 @@ export function App() {
|
|
|
784
854
|
case 1: // Stripe API Key
|
|
785
855
|
setInputMode("edit_stripe_key");
|
|
786
856
|
break;
|
|
787
|
-
case 2: //
|
|
857
|
+
case 2: // Theme
|
|
858
|
+
setInputMode("select_theme");
|
|
859
|
+
break;
|
|
860
|
+
case 3: // Timezone
|
|
788
861
|
setInputMode("edit_timezone");
|
|
789
862
|
break;
|
|
790
|
-
case
|
|
863
|
+
case 4: // Export Database
|
|
791
864
|
handleExportDatabase();
|
|
792
865
|
break;
|
|
793
|
-
case
|
|
866
|
+
case 5: // Import Database
|
|
794
867
|
setConfirmMessage("Import will replace all data. Continue?");
|
|
795
868
|
setConfirmAction(() => () => handleImportDatabase());
|
|
796
869
|
break;
|
|
@@ -806,6 +879,84 @@ export function App() {
|
|
|
806
879
|
};
|
|
807
880
|
|
|
808
881
|
const handleTimesheetKeyboard = (key: { name: string }) => {
|
|
882
|
+
// Toggle between default timesheets and all timers view
|
|
883
|
+
if (key.name === "a") {
|
|
884
|
+
setShowAllTimers((prev) => {
|
|
885
|
+
const newValue = !prev;
|
|
886
|
+
if (newValue) {
|
|
887
|
+
showMessage("Showing all timers by week");
|
|
888
|
+
loadAllTimers();
|
|
889
|
+
} else {
|
|
890
|
+
showMessage("Showing billable timesheets");
|
|
891
|
+
loadTimesheets();
|
|
892
|
+
}
|
|
893
|
+
return newValue;
|
|
894
|
+
});
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Handle all timers mode
|
|
899
|
+
if (showAllTimers) {
|
|
900
|
+
if (!allTimersWeekData) return;
|
|
901
|
+
const maxIndex = allTimersWeekData.entries.length - 1;
|
|
902
|
+
|
|
903
|
+
// Navigation within entries
|
|
904
|
+
if (key.name === "j" || key.name === "down") {
|
|
905
|
+
setAllTimersSelectedIndex((i) => Math.min(i + 1, maxIndex));
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (key.name === "k" || key.name === "up") {
|
|
909
|
+
setAllTimersSelectedIndex((i) => Math.max(i - 1, 0));
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Week navigation
|
|
914
|
+
if (key.name === "[" && hasOlderWeeks) {
|
|
915
|
+
const prevWeekStart = new Date(allTimersWeekData.weekStart);
|
|
916
|
+
prevWeekStart.setDate(prevWeekStart.getDate() - 7);
|
|
917
|
+
loadAllTimers(prevWeekStart);
|
|
918
|
+
setAllTimersSelectedIndex(0);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (key.name === "]" && hasNewerWeeks) {
|
|
922
|
+
const nextWeekStart = new Date(allTimersWeekData.weekStart);
|
|
923
|
+
nextWeekStart.setDate(nextWeekStart.getDate() + 7);
|
|
924
|
+
loadAllTimers(nextWeekStart);
|
|
925
|
+
setAllTimersSelectedIndex(0);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Edit entry
|
|
930
|
+
if (key.name === "e") {
|
|
931
|
+
const entry = allTimersWeekData.entries[allTimersSelectedIndex];
|
|
932
|
+
if (entry) {
|
|
933
|
+
setEditingTimeEntry(entry as typeof editingTimeEntry);
|
|
934
|
+
setInputMode("edit_time_entry");
|
|
935
|
+
}
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Delete entry
|
|
940
|
+
if (key.name === "d") {
|
|
941
|
+
const entry = allTimersWeekData.entries[allTimersSelectedIndex];
|
|
942
|
+
if (entry) {
|
|
943
|
+
setConfirmMessage(
|
|
944
|
+
`Delete time entry from ${entry.project.name}?`,
|
|
945
|
+
);
|
|
946
|
+
setConfirmAction(() => () => {
|
|
947
|
+
timeEntries.delete(entry.id).then(() => {
|
|
948
|
+
showMessage("Time entry deleted");
|
|
949
|
+
loadAllTimers(allTimersWeekData.weekStart);
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Default timesheets mode
|
|
809
960
|
const currentGroup = timesheetGroups[selectedTimesheetGroupIndex];
|
|
810
961
|
if (!currentGroup) return;
|
|
811
962
|
|
|
@@ -1018,6 +1169,14 @@ export function App() {
|
|
|
1018
1169
|
showMessage(`Timezone set to ${value === "auto" ? "auto-detect" : value}`);
|
|
1019
1170
|
};
|
|
1020
1171
|
|
|
1172
|
+
const handleSelectTheme = async (themeName: string) => {
|
|
1173
|
+
await settings.set("theme", themeName);
|
|
1174
|
+
setAppSettings((prev) => ({ ...prev, theme: themeName }));
|
|
1175
|
+
const theme = getTheme(themeName);
|
|
1176
|
+
setInputMode(null);
|
|
1177
|
+
showMessage(`Theme: ${theme.displayName}`);
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1021
1180
|
// Modal handlers
|
|
1022
1181
|
const handleCreateProject = async (name: string, rate: number | null) => {
|
|
1023
1182
|
await projects.create({ name, hourlyRate: rate });
|
|
@@ -1134,7 +1293,11 @@ export function App() {
|
|
|
1134
1293
|
showMessage("Time entry updated");
|
|
1135
1294
|
setInputMode(null);
|
|
1136
1295
|
setEditingTimeEntry(null);
|
|
1137
|
-
|
|
1296
|
+
if (showAllTimers) {
|
|
1297
|
+
loadAllTimers(allTimersWeekData?.weekStart);
|
|
1298
|
+
} else {
|
|
1299
|
+
loadTimesheets();
|
|
1300
|
+
}
|
|
1138
1301
|
};
|
|
1139
1302
|
|
|
1140
1303
|
const handleCreateInvoice = async () => {
|
|
@@ -1232,6 +1395,7 @@ export function App() {
|
|
|
1232
1395
|
};
|
|
1233
1396
|
|
|
1234
1397
|
const selectedProject = projectList[selectedProjectIndex];
|
|
1398
|
+
const theme = getTheme(appSettings.theme);
|
|
1235
1399
|
|
|
1236
1400
|
return (
|
|
1237
1401
|
<box
|
|
@@ -1239,6 +1403,7 @@ export function App() {
|
|
|
1239
1403
|
width: "100%",
|
|
1240
1404
|
height: "100%",
|
|
1241
1405
|
flexDirection: "column",
|
|
1406
|
+
backgroundColor: theme.colors.bg,
|
|
1242
1407
|
}}
|
|
1243
1408
|
>
|
|
1244
1409
|
<Header
|
|
@@ -1246,6 +1411,7 @@ export function App() {
|
|
|
1246
1411
|
onViewChange={setCurrentView}
|
|
1247
1412
|
runningTimer={runningTimer}
|
|
1248
1413
|
onStopTimer={() => setInputMode("stop_timer")}
|
|
1414
|
+
theme={getTheme(appSettings.theme)}
|
|
1249
1415
|
/>
|
|
1250
1416
|
|
|
1251
1417
|
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
@@ -1256,18 +1422,21 @@ export function App() {
|
|
|
1256
1422
|
weeklyTimeData={weeklyTimeData}
|
|
1257
1423
|
selectedIndex={selectedDashboardTaskIndex}
|
|
1258
1424
|
focused={true}
|
|
1425
|
+
theme={getTheme(appSettings.theme)}
|
|
1259
1426
|
/>
|
|
1260
1427
|
)}
|
|
1261
1428
|
|
|
1262
|
-
{currentView === "help" && <HelpView />}
|
|
1429
|
+
{currentView === "help" && <HelpView theme={getTheme(appSettings.theme)} />}
|
|
1263
1430
|
|
|
1264
1431
|
{currentView === "settings" && (
|
|
1265
1432
|
<SettingsView
|
|
1266
1433
|
settings={appSettings}
|
|
1267
1434
|
selectedIndex={selectedSettingsIndex}
|
|
1435
|
+
theme={getTheme(appSettings.theme)}
|
|
1268
1436
|
onEditBusinessName={() => setInputMode("edit_business_name")}
|
|
1269
1437
|
onEditStripeKey={() => setInputMode("edit_stripe_key")}
|
|
1270
1438
|
onEditTimezone={() => setInputMode("edit_timezone")}
|
|
1439
|
+
onSelectTheme={() => setInputMode("select_theme")}
|
|
1271
1440
|
onExportDatabase={handleExportDatabase}
|
|
1272
1441
|
onImportDatabase={() => {
|
|
1273
1442
|
setConfirmMessage("Import will replace all data. Continue?");
|
|
@@ -1284,6 +1453,12 @@ export function App() {
|
|
|
1284
1453
|
selectedEntryIds={selectedTimeEntryIds}
|
|
1285
1454
|
focused={true}
|
|
1286
1455
|
timezone={getEffectiveTimezone(appSettings)}
|
|
1456
|
+
theme={getTheme(appSettings.theme)}
|
|
1457
|
+
showAllTimers={showAllTimers}
|
|
1458
|
+
allTimersWeekData={allTimersWeekData}
|
|
1459
|
+
allTimersSelectedIndex={allTimersSelectedIndex}
|
|
1460
|
+
hasOlderWeeks={hasOlderWeeks}
|
|
1461
|
+
hasNewerWeeks={hasNewerWeeks}
|
|
1287
1462
|
/>
|
|
1288
1463
|
)}
|
|
1289
1464
|
|
|
@@ -1298,6 +1473,7 @@ export function App() {
|
|
|
1298
1473
|
currentPage={invoicesPage}
|
|
1299
1474
|
hasMore={invoicesHasMore}
|
|
1300
1475
|
hasPrevious={invoicesCursors.length > 0}
|
|
1476
|
+
theme={getTheme(appSettings.theme)}
|
|
1301
1477
|
/>
|
|
1302
1478
|
)}
|
|
1303
1479
|
|
|
@@ -1309,6 +1485,7 @@ export function App() {
|
|
|
1309
1485
|
selectedIndex={selectedProjectIndex}
|
|
1310
1486
|
focused={activePanel === "projects"}
|
|
1311
1487
|
showArchived={showArchived}
|
|
1488
|
+
theme={getTheme(appSettings.theme)}
|
|
1312
1489
|
/>
|
|
1313
1490
|
</box>
|
|
1314
1491
|
<box style={{ width: "60%", flexDirection: "column" }}>
|
|
@@ -1317,6 +1494,7 @@ export function App() {
|
|
|
1317
1494
|
selectedIndex={selectedTaskIndex}
|
|
1318
1495
|
focused={activePanel === "tasks"}
|
|
1319
1496
|
projectName={selectedProject?.name}
|
|
1497
|
+
theme={getTheme(appSettings.theme)}
|
|
1320
1498
|
/>
|
|
1321
1499
|
</box>
|
|
1322
1500
|
</>
|
|
@@ -1329,6 +1507,22 @@ export function App() {
|
|
|
1329
1507
|
timerRunning={!!runningTimer}
|
|
1330
1508
|
currentView={currentView}
|
|
1331
1509
|
activePanel={activePanel}
|
|
1510
|
+
theme={getTheme(appSettings.theme)}
|
|
1511
|
+
showAllTimers={currentView === "timesheets" ? showAllTimers : undefined}
|
|
1512
|
+
allTimersWeekRange={
|
|
1513
|
+
currentView === "timesheets" && showAllTimers && allTimersWeekData
|
|
1514
|
+
? (() => {
|
|
1515
|
+
const tz = getEffectiveTimezone(appSettings);
|
|
1516
|
+
const startStr = formatDateInTimezone(allTimersWeekData.weekStart, tz);
|
|
1517
|
+
const endDate = new Date(allTimersWeekData.weekEnd);
|
|
1518
|
+
endDate.setDate(endDate.getDate() - 1);
|
|
1519
|
+
const endStr = formatDateInTimezone(endDate, tz);
|
|
1520
|
+
return `${startStr} - ${endStr}`;
|
|
1521
|
+
})()
|
|
1522
|
+
: undefined
|
|
1523
|
+
}
|
|
1524
|
+
hasOlderWeeks={currentView === "timesheets" && showAllTimers ? hasOlderWeeks : undefined}
|
|
1525
|
+
hasNewerWeeks={currentView === "timesheets" && showAllTimers ? hasNewerWeeks : undefined}
|
|
1332
1526
|
/>
|
|
1333
1527
|
|
|
1334
1528
|
{/* Modals */}
|
|
@@ -1337,6 +1531,7 @@ export function App() {
|
|
|
1337
1531
|
mode="create"
|
|
1338
1532
|
onSubmit={handleCreateProject}
|
|
1339
1533
|
onCancel={() => setInputMode(null)}
|
|
1534
|
+
theme={theme}
|
|
1340
1535
|
/>
|
|
1341
1536
|
)}
|
|
1342
1537
|
|
|
@@ -1347,6 +1542,7 @@ export function App() {
|
|
|
1347
1542
|
initialRate={selectedProject.hourlyRate}
|
|
1348
1543
|
onSubmit={handleEditProject}
|
|
1349
1544
|
onCancel={() => setInputMode(null)}
|
|
1545
|
+
theme={theme}
|
|
1350
1546
|
/>
|
|
1351
1547
|
)}
|
|
1352
1548
|
|
|
@@ -1357,6 +1553,7 @@ export function App() {
|
|
|
1357
1553
|
placeholder="Task title..."
|
|
1358
1554
|
onSubmit={handleCreateTask}
|
|
1359
1555
|
onCancel={() => setInputMode(null)}
|
|
1556
|
+
theme={theme}
|
|
1360
1557
|
/>
|
|
1361
1558
|
)}
|
|
1362
1559
|
|
|
@@ -1368,6 +1565,7 @@ export function App() {
|
|
|
1368
1565
|
placeholder="Task title..."
|
|
1369
1566
|
onSubmit={handleEditTask}
|
|
1370
1567
|
onCancel={() => setInputMode(null)}
|
|
1568
|
+
theme={theme}
|
|
1371
1569
|
/>
|
|
1372
1570
|
)}
|
|
1373
1571
|
|
|
@@ -1380,6 +1578,7 @@ export function App() {
|
|
|
1380
1578
|
placeholder="Task title..."
|
|
1381
1579
|
onSubmit={handleEditDashboardTask}
|
|
1382
1580
|
onCancel={() => setInputMode(null)}
|
|
1581
|
+
theme={theme}
|
|
1383
1582
|
/>
|
|
1384
1583
|
)}
|
|
1385
1584
|
|
|
@@ -1391,6 +1590,7 @@ export function App() {
|
|
|
1391
1590
|
placeholder="Enter business name..."
|
|
1392
1591
|
onSubmit={handleUpdateBusinessName}
|
|
1393
1592
|
onCancel={() => setInputMode(null)}
|
|
1593
|
+
theme={theme}
|
|
1394
1594
|
/>
|
|
1395
1595
|
)}
|
|
1396
1596
|
|
|
@@ -1402,6 +1602,7 @@ export function App() {
|
|
|
1402
1602
|
placeholder="sk_live_..."
|
|
1403
1603
|
onSubmit={handleUpdateStripeKey}
|
|
1404
1604
|
onCancel={() => setInputMode(null)}
|
|
1605
|
+
theme={theme}
|
|
1405
1606
|
/>
|
|
1406
1607
|
)}
|
|
1407
1608
|
|
|
@@ -1413,6 +1614,15 @@ export function App() {
|
|
|
1413
1614
|
placeholder="America/New_York, Europe/London, auto..."
|
|
1414
1615
|
onSubmit={handleUpdateTimezone}
|
|
1415
1616
|
onCancel={() => setInputMode(null)}
|
|
1617
|
+
theme={theme}
|
|
1618
|
+
/>
|
|
1619
|
+
)}
|
|
1620
|
+
|
|
1621
|
+
{inputMode === "select_theme" && (
|
|
1622
|
+
<ThemeSelectModal
|
|
1623
|
+
currentTheme={appSettings.theme}
|
|
1624
|
+
onSelect={handleSelectTheme}
|
|
1625
|
+
onCancel={() => setInputMode(null)}
|
|
1416
1626
|
/>
|
|
1417
1627
|
)}
|
|
1418
1628
|
|
|
@@ -1421,6 +1631,7 @@ export function App() {
|
|
|
1421
1631
|
projects={projectList.filter((p) => !p.archived)}
|
|
1422
1632
|
onSelect={handleStartTimer}
|
|
1423
1633
|
onCancel={() => setInputMode(null)}
|
|
1634
|
+
theme={theme}
|
|
1424
1635
|
/>
|
|
1425
1636
|
)}
|
|
1426
1637
|
|
|
@@ -1431,6 +1642,7 @@ export function App() {
|
|
|
1431
1642
|
duration={formatDuration(timerElapsed)}
|
|
1432
1643
|
onSubmit={handleStopTimer}
|
|
1433
1644
|
onCancel={() => setInputMode(null)}
|
|
1645
|
+
theme={theme}
|
|
1434
1646
|
/>
|
|
1435
1647
|
)}
|
|
1436
1648
|
|
|
@@ -1446,6 +1658,7 @@ export function App() {
|
|
|
1446
1658
|
setInputMode("edit_customer");
|
|
1447
1659
|
}}
|
|
1448
1660
|
onCancel={() => setInputMode(null)}
|
|
1661
|
+
theme={theme}
|
|
1449
1662
|
/>
|
|
1450
1663
|
)}
|
|
1451
1664
|
|
|
@@ -1454,6 +1667,7 @@ export function App() {
|
|
|
1454
1667
|
mode="create"
|
|
1455
1668
|
onSubmit={handleCreateCustomer}
|
|
1456
1669
|
onCancel={() => setInputMode("select_customer")}
|
|
1670
|
+
theme={theme}
|
|
1457
1671
|
/>
|
|
1458
1672
|
)}
|
|
1459
1673
|
|
|
@@ -1468,6 +1682,7 @@ export function App() {
|
|
|
1468
1682
|
setEditingCustomer(null);
|
|
1469
1683
|
setInputMode("select_customer");
|
|
1470
1684
|
}}
|
|
1685
|
+
theme={theme}
|
|
1471
1686
|
/>
|
|
1472
1687
|
)}
|
|
1473
1688
|
|
|
@@ -1480,6 +1695,7 @@ export function App() {
|
|
|
1480
1695
|
setInputMode(null);
|
|
1481
1696
|
setEditingTimeEntry(null);
|
|
1482
1697
|
}}
|
|
1698
|
+
theme={theme}
|
|
1483
1699
|
/>
|
|
1484
1700
|
)}
|
|
1485
1701
|
|
|
@@ -1511,6 +1727,7 @@ export function App() {
|
|
|
1511
1727
|
hasStripeKey={!!appSettings.stripeApiKey}
|
|
1512
1728
|
onConfirm={handleCreateInvoice}
|
|
1513
1729
|
onCancel={() => setInputMode(null)}
|
|
1730
|
+
theme={theme}
|
|
1514
1731
|
/>
|
|
1515
1732
|
)}
|
|
1516
1733
|
|
|
@@ -1527,6 +1744,7 @@ export function App() {
|
|
|
1527
1744
|
setConfirmAction(null);
|
|
1528
1745
|
setConfirmMessage("");
|
|
1529
1746
|
}}
|
|
1747
|
+
theme={theme}
|
|
1530
1748
|
/>
|
|
1531
1749
|
)}
|
|
1532
1750
|
</box>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { useKeyboard } from "@opentui/react";
|
|
2
2
|
import {
|
|
3
|
+
CATPPUCCIN_MOCHA,
|
|
3
4
|
type Customer,
|
|
4
5
|
type TimeEntryWithProject,
|
|
6
|
+
type Theme,
|
|
5
7
|
formatDateInTimezone,
|
|
6
8
|
} from "../types.ts";
|
|
7
9
|
import Modal from "./Modal.tsx";
|
|
@@ -16,6 +18,7 @@ interface CreateInvoiceModalProps {
|
|
|
16
18
|
hasStripeKey: boolean;
|
|
17
19
|
onConfirm: () => void;
|
|
18
20
|
onCancel: () => void;
|
|
21
|
+
theme?: Theme;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
function formatDuration(ms: number): string {
|
|
@@ -44,7 +47,10 @@ export function CreateInvoiceModal({
|
|
|
44
47
|
hasStripeKey,
|
|
45
48
|
onConfirm,
|
|
46
49
|
onCancel,
|
|
50
|
+
theme = CATPPUCCIN_MOCHA,
|
|
47
51
|
}: CreateInvoiceModalProps) {
|
|
52
|
+
const colors = theme.colors;
|
|
53
|
+
|
|
48
54
|
useKeyboard((key) => {
|
|
49
55
|
if (key.name === "escape") {
|
|
50
56
|
onCancel();
|
|
@@ -97,20 +103,20 @@ export function CreateInvoiceModal({
|
|
|
97
103
|
: baseHeight + lineItemsHeight;
|
|
98
104
|
|
|
99
105
|
return (
|
|
100
|
-
<Modal title="Create Stripe Invoice" height={Math.max(modalHeight, 20)}>
|
|
106
|
+
<Modal title="Create Stripe Invoice" height={Math.max(modalHeight, 20)} theme={theme}>
|
|
101
107
|
{/* Customer info */}
|
|
102
108
|
{customer && (
|
|
103
109
|
<box style={{ flexDirection: "row", marginTop: 1, gap: 1 }}>
|
|
104
|
-
<text fg=
|
|
110
|
+
<text fg={colors.accentSecondary} attributes="bold">
|
|
105
111
|
{customer.name}
|
|
106
112
|
</text>
|
|
107
|
-
<text fg=
|
|
113
|
+
<text fg={colors.textMuted}>{customer.email}</text>
|
|
108
114
|
</box>
|
|
109
115
|
)}
|
|
110
116
|
|
|
111
117
|
{!customer && (
|
|
112
118
|
<box style={{ marginTop: 1 }}>
|
|
113
|
-
<text fg=
|
|
119
|
+
<text fg={colors.error}>
|
|
114
120
|
This project has no customer linked. Please link a customer first
|
|
115
121
|
using the 'c' key in the Projects view.
|
|
116
122
|
</text>
|
|
@@ -119,7 +125,7 @@ export function CreateInvoiceModal({
|
|
|
119
125
|
|
|
120
126
|
{!hasStripeKey && customer && (
|
|
121
127
|
<box style={{ marginTop: 1 }}>
|
|
122
|
-
<text fg=
|
|
128
|
+
<text fg={colors.error}>
|
|
123
129
|
No Stripe API key configured. Add one in Settings to create
|
|
124
130
|
invoices.
|
|
125
131
|
</text>
|
|
@@ -129,22 +135,22 @@ export function CreateInvoiceModal({
|
|
|
129
135
|
{customer && hasStripeKey && (
|
|
130
136
|
<>
|
|
131
137
|
{/* Line items header */}
|
|
132
|
-
<text fg=
|
|
138
|
+
<text fg={colors.borderSubtle} style={{ marginTop: 1 }}>
|
|
133
139
|
{"─".repeat(56)}
|
|
134
140
|
</text>
|
|
135
141
|
<box style={{ flexDirection: "row", gap: 1 }}>
|
|
136
142
|
<box style={{ width: 8 }}>
|
|
137
|
-
<text fg=
|
|
143
|
+
<text fg={colors.accentSecondary}>Date</text>
|
|
138
144
|
</box>
|
|
139
145
|
<box style={{ flexGrow: 1 }}>
|
|
140
|
-
<text fg=
|
|
146
|
+
<text fg={colors.accentSecondary}>Description</text>
|
|
141
147
|
</box>
|
|
142
148
|
<box style={{ width: 8 }}>
|
|
143
|
-
<text fg=
|
|
149
|
+
<text fg={colors.accentSecondary}>Hours</text>
|
|
144
150
|
</box>
|
|
145
151
|
{hourlyRate != null && (
|
|
146
152
|
<box style={{ width: 10, alignItems: "flex-end" }}>
|
|
147
|
-
<text fg=
|
|
153
|
+
<text fg={colors.accentSecondary}>Amount</text>
|
|
148
154
|
</box>
|
|
149
155
|
)}
|
|
150
156
|
</box>
|
|
@@ -154,21 +160,21 @@ export function CreateInvoiceModal({
|
|
|
154
160
|
{lineItems.map((item, idx) => (
|
|
155
161
|
<box key={idx} style={{ flexDirection: "row", gap: 1 }}>
|
|
156
162
|
<box style={{ width: 8 }}>
|
|
157
|
-
<text fg=
|
|
163
|
+
<text fg={colors.textMuted}>{item.date}</text>
|
|
158
164
|
</box>
|
|
159
165
|
<box style={{ flexGrow: 1 }}>
|
|
160
|
-
<text fg=
|
|
166
|
+
<text fg={colors.textPrimary}>
|
|
161
167
|
{item.description.length > 30
|
|
162
168
|
? `${item.description.slice(0, 27)}...`
|
|
163
169
|
: item.description}
|
|
164
170
|
</text>
|
|
165
171
|
</box>
|
|
166
172
|
<box style={{ width: 8 }}>
|
|
167
|
-
<text fg=
|
|
173
|
+
<text fg={colors.accentSecondary}>{item.hours}</text>
|
|
168
174
|
</box>
|
|
169
175
|
{hourlyRate != null && (
|
|
170
176
|
<box style={{ width: 10, alignItems: "flex-end" }}>
|
|
171
|
-
<text fg=
|
|
177
|
+
<text fg={colors.success}>${item.amount.toFixed(2)}</text>
|
|
172
178
|
</box>
|
|
173
179
|
)}
|
|
174
180
|
</box>
|
|
@@ -176,26 +182,26 @@ export function CreateInvoiceModal({
|
|
|
176
182
|
</scrollbox>
|
|
177
183
|
|
|
178
184
|
{/* Totals */}
|
|
179
|
-
<text fg=
|
|
185
|
+
<text fg={colors.borderSubtle}>{"─".repeat(56)}</text>
|
|
180
186
|
<box style={{ flexDirection: "row", gap: 1 }}>
|
|
181
187
|
<box style={{ width: 8 }}>
|
|
182
|
-
<text fg=
|
|
188
|
+
<text fg={colors.textPrimary} attributes="bold">
|
|
183
189
|
Total
|
|
184
190
|
</text>
|
|
185
191
|
</box>
|
|
186
192
|
<box style={{ flexGrow: 1 }}>
|
|
187
|
-
<text fg=
|
|
193
|
+
<text fg={colors.accentSecondary}>
|
|
188
194
|
{entries.length} {entries.length === 1 ? "entry" : "entries"}
|
|
189
195
|
</text>
|
|
190
196
|
</box>
|
|
191
197
|
<box style={{ width: 8 }}>
|
|
192
|
-
<text fg=
|
|
198
|
+
<text fg={colors.textPrimary} attributes="bold">
|
|
193
199
|
{totalHours.toFixed(2)}
|
|
194
200
|
</text>
|
|
195
201
|
</box>
|
|
196
202
|
{hourlyRate != null && (
|
|
197
203
|
<box style={{ width: 10, alignItems: "flex-end" }}>
|
|
198
|
-
<text fg=
|
|
204
|
+
<text fg={colors.success} attributes="bold">
|
|
199
205
|
${totalAmount.toFixed(2)}
|
|
200
206
|
</text>
|
|
201
207
|
</box>
|
|
@@ -204,7 +210,7 @@ export function CreateInvoiceModal({
|
|
|
204
210
|
|
|
205
211
|
{hourlyRate != null && (
|
|
206
212
|
<box style={{ marginTop: 1 }}>
|
|
207
|
-
<text fg=
|
|
213
|
+
<text fg={colors.textMuted}>Rate: ${hourlyRate.toFixed(2)}/hr</text>
|
|
208
214
|
</box>
|
|
209
215
|
)}
|
|
210
216
|
</>
|
|
@@ -212,7 +218,7 @@ export function CreateInvoiceModal({
|
|
|
212
218
|
|
|
213
219
|
<box style={{ flexGrow: 1 }} />
|
|
214
220
|
|
|
215
|
-
<text fg=
|
|
221
|
+
<text fg={colors.textMuted}>
|
|
216
222
|
{customer && hasStripeKey
|
|
217
223
|
? "Enter to create draft invoice, Esc to cancel"
|
|
218
224
|
: "Esc to close"}
|