Cleaned up the theme a bit
This commit is contained in:
@@ -3,4 +3,4 @@ ROOT_DIR=./data
|
|||||||
APP_PORT=4321
|
APP_PORT=4321
|
||||||
IMAGE=git.atri.dad/atash/chronus:latest
|
IMAGE=git.atri.dad/atash/chronus:latest
|
||||||
JWT_SECRET=some-secret
|
JWT_SECRET=some-secret
|
||||||
ORIGIN=https://chronus.example.com
|
ORIGIN=https://chronus.example.com
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
"vue-chartjs": "^5.3.3"
|
"vue-chartjs": "^5.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@catppuccin/daisyui": "^2.1.1",
|
|
||||||
"@react-pdf/types": "^2.9.2",
|
"@react-pdf/types": "^2.9.2",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"drizzle-kit": "0.31.9"
|
"drizzle-kit": "0.31.9"
|
||||||
|
|||||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -69,9 +69,6 @@ importers:
|
|||||||
specifier: ^5.3.3
|
specifier: ^5.3.3
|
||||||
version: 5.3.3(chart.js@4.5.1)(vue@3.5.28(typescript@5.9.3))
|
version: 5.3.3(chart.js@4.5.1)(vue@3.5.28(typescript@5.9.3))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@catppuccin/daisyui':
|
|
||||||
specifier: ^2.1.1
|
|
||||||
version: 2.1.1(tailwindcss@4.1.18)
|
|
||||||
'@react-pdf/types':
|
'@react-pdf/types':
|
||||||
specifier: ^2.9.2
|
specifier: ^2.9.2
|
||||||
version: 2.9.2
|
version: 2.9.2
|
||||||
@@ -285,14 +282,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==}
|
resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@catppuccin/daisyui@2.1.1':
|
|
||||||
resolution: {integrity: sha512-PrZttjj8kwfDBJ34sR+DN25Xtjvxx4T5p8uu/iiGYZR8UOsNwzMlO/alYDBwwTOLzP1NKLNRax09kCT39+QM+A==}
|
|
||||||
peerDependencies:
|
|
||||||
tailwindcss: ^4.0.17
|
|
||||||
|
|
||||||
'@catppuccin/palette@1.7.1':
|
|
||||||
resolution: {integrity: sha512-aRc1tbzrevOTV7nFTT9SRdF26w/MIwT4Jwt4fDMc9itRZUDXCuEDBLyz4TQMlqO9ZP8mf5Hu4Jr6D03NLFc6Gw==}
|
|
||||||
|
|
||||||
'@ceereals/vue-pdf@0.2.1':
|
'@ceereals/vue-pdf@0.2.1':
|
||||||
resolution: {integrity: sha512-E7Y2GyHTYEmZ2U5ZlVuJrOWdHhco49ZTdKVOo/wcOhlfNFG+W5pAZ6rOcaua+owspC4BgGzAxlmqj/jdEM9ehA==}
|
resolution: {integrity: sha512-E7Y2GyHTYEmZ2U5ZlVuJrOWdHhco49ZTdKVOo/wcOhlfNFG+W5pAZ6rOcaua+owspC4BgGzAxlmqj/jdEM9ehA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4031,13 +4020,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fontkitten: 1.0.2
|
fontkitten: 1.0.2
|
||||||
|
|
||||||
'@catppuccin/daisyui@2.1.1(tailwindcss@4.1.18)':
|
|
||||||
dependencies:
|
|
||||||
'@catppuccin/palette': 1.7.1
|
|
||||||
tailwindcss: 4.1.18
|
|
||||||
|
|
||||||
'@catppuccin/palette@1.7.1': {}
|
|
||||||
|
|
||||||
'@ceereals/vue-pdf@0.2.1(vue@3.5.28(typescript@5.9.3))':
|
'@ceereals/vue-pdf@0.2.1(vue@3.5.28(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@react-pdf/fns': 3.1.2
|
'@react-pdf/fns': 3.1.2
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const initial = name ? name.charAt(0).toUpperCase() : '?';
|
|||||||
---
|
---
|
||||||
|
|
||||||
<div class:list={["avatar placeholder", className]}>
|
<div class:list={["avatar placeholder", className]}>
|
||||||
<div class="bg-primary/15 text-primary w-9 h-9 rounded-full flex items-center justify-center">
|
<div class="bg-base-300 text-primary w-9 h-9 rounded-full flex items-center justify-center">
|
||||||
<span class="text-sm font-semibold">{initial}</span>
|
<span class="text-sm font-semibold">{initial}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ function clearForm() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="card bg-base-200/50 backdrop-blur-sm shadow-lg border border-base-300/50 hover:border-base-300 transition-all duration-200"
|
class="card bg-base-200 backdrop-blur-sm shadow-lg border border-base-content/20 hover:border-base-content/20 transition-all duration-200"
|
||||||
>
|
>
|
||||||
<div class="card-body gap-6">
|
<div class="card-body gap-6">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
@@ -183,7 +183,7 @@ function clearForm() {
|
|||||||
<select
|
<select
|
||||||
id="manual-client"
|
id="manual-client"
|
||||||
v-model="selectedClientId"
|
v-model="selectedClientId"
|
||||||
class="select select-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
class="select select-bordered w-full bg-base-300 hover:bg-base-300 focus:bg-base-300 border-base-content/20 focus:border-primary transition-colors"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
>
|
||||||
<option value="">Select a client...</option>
|
<option value="">Select a client...</option>
|
||||||
@@ -203,7 +203,7 @@ function clearForm() {
|
|||||||
id="manual-start-date"
|
id="manual-start-date"
|
||||||
v-model="startDate"
|
v-model="startDate"
|
||||||
type="date"
|
type="date"
|
||||||
class="input input-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
class="input input-bordered w-full bg-base-300 hover:bg-base-300 focus:bg-base-300 border-base-content/20 focus:border-primary transition-colors"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,7 +216,7 @@ function clearForm() {
|
|||||||
id="manual-start-time"
|
id="manual-start-time"
|
||||||
v-model="startTime"
|
v-model="startTime"
|
||||||
type="time"
|
type="time"
|
||||||
class="input input-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
class="input input-bordered w-full bg-base-300 hover:bg-base-300 focus:bg-base-300 border-base-content/20 focus:border-primary transition-colors"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,7 +232,7 @@ function clearForm() {
|
|||||||
id="manual-end-date"
|
id="manual-end-date"
|
||||||
v-model="endDate"
|
v-model="endDate"
|
||||||
type="date"
|
type="date"
|
||||||
class="input input-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
class="input input-bordered w-full bg-base-300 hover:bg-base-300 focus:bg-base-300 border-base-content/20 focus:border-primary transition-colors"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -245,7 +245,7 @@ function clearForm() {
|
|||||||
id="manual-end-time"
|
id="manual-end-time"
|
||||||
v-model="endTime"
|
v-model="endTime"
|
||||||
type="time"
|
type="time"
|
||||||
class="input input-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
class="input input-bordered w-full bg-base-300 hover:bg-base-300 focus:bg-base-300 border-base-content/20 focus:border-primary transition-colors"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -261,7 +261,7 @@ function clearForm() {
|
|||||||
v-model="description"
|
v-model="description"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="What did you work on?"
|
placeholder="What did you work on?"
|
||||||
class="input input-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
class="input input-bordered w-full bg-base-300 hover:bg-base-300 focus:bg-base-300 border-base-content/20 focus:border-primary transition-colors"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,7 +278,7 @@ function clearForm() {
|
|||||||
'badge badge-lg cursor-pointer transition-all hover:scale-105',
|
'badge badge-lg cursor-pointer transition-all hover:scale-105',
|
||||||
selectedTagId === tag.id
|
selectedTagId === tag.id
|
||||||
? 'badge-primary shadow-lg shadow-primary/20'
|
? 'badge-primary shadow-lg shadow-primary/20'
|
||||||
: 'badge-outline hover:bg-base-300/50',
|
: 'badge-outline hover:bg-base-300',
|
||||||
]"
|
]"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ const { title, value, description, icon, color = 'text-primary', valueClass } =
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-xs font-medium uppercase tracking-wider text-base-content/60">{title}</span>
|
<span class="text-xs font-medium uppercase tracking-wider text-base-content/60">{title}</span>
|
||||||
{icon && (
|
{icon && (
|
||||||
<div class:list={[color, "opacity-40"]}>
|
<div class:list={[color, "opacity-70"]}>
|
||||||
<Icon name={icon} class="w-5 h-5" />
|
<Icon name={icon} class="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class:list={["text-2xl font-bold", color, valueClass]}>{value}</div>
|
<div class:list={["text-2xl font-bold", color, valueClass]}>{value}</div>
|
||||||
{description && <div class="text-xs text-base-content/50">{description}</div>}
|
{description && <div class="text-xs text-base-content/60">{description}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,34 +1,31 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from "vue";
|
||||||
import Icon from './Icon.vue';
|
import Icon from "./Icon.vue";
|
||||||
|
|
||||||
const theme = ref('macchiato');
|
const theme = ref("sunset");
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const stored = localStorage.getItem('theme');
|
const stored = localStorage.getItem("theme");
|
||||||
if (stored) {
|
if (stored) {
|
||||||
theme.value = stored;
|
theme.value = stored;
|
||||||
document.documentElement.setAttribute('data-theme', stored);
|
document.documentElement.setAttribute("data-theme", stored);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
const newTheme = theme.value === 'macchiato' ? 'latte' : 'macchiato';
|
const newTheme = theme.value === "sunset" ? "winter" : "sunset";
|
||||||
theme.value = newTheme;
|
theme.value = newTheme;
|
||||||
document.documentElement.setAttribute('data-theme', newTheme);
|
document.documentElement.setAttribute("data-theme", newTheme);
|
||||||
localStorage.setItem('theme', newTheme);
|
localStorage.setItem("theme", newTheme);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
@click="toggleTheme"
|
@click="toggleTheme"
|
||||||
class="btn btn-ghost btn-circle"
|
class="btn btn-ghost btn-circle"
|
||||||
aria-label="Toggle Theme"
|
aria-label="Toggle Theme"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon :name="theme === 'sunset' ? 'moon' : 'sun'" class="w-5 h-5" />
|
||||||
:name="theme === 'macchiato' ? 'moon' : 'sun'"
|
|
||||||
class="w-5 h-5"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ async function stopTimer() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="card bg-base-200/50 backdrop-blur-sm shadow-lg border border-base-300/50 mb-6 hover:border-base-300 transition-all duration-200"
|
class="card bg-base-200 backdrop-blur-sm shadow-lg border border-base-content/20 mb-6 hover:border-base-content/20 transition-all duration-200"
|
||||||
>
|
>
|
||||||
<div class="card-body gap-6">
|
<div class="card-body gap-6">
|
||||||
<!-- Client Row -->
|
<!-- Client Row -->
|
||||||
@@ -129,7 +129,7 @@ async function stopTimer() {
|
|||||||
<select
|
<select
|
||||||
id="timer-client"
|
id="timer-client"
|
||||||
v-model="selectedClientId"
|
v-model="selectedClientId"
|
||||||
class="select select-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
class="select select-bordered w-full bg-base-300 hover:bg-base-300 focus:bg-base-300 border-base-content/20 focus:border-primary transition-colors"
|
||||||
:disabled="isRunning"
|
:disabled="isRunning"
|
||||||
>
|
>
|
||||||
<option value="">Select a client...</option>
|
<option value="">Select a client...</option>
|
||||||
@@ -149,7 +149,7 @@ async function stopTimer() {
|
|||||||
v-model="description"
|
v-model="description"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="What are you working on?"
|
placeholder="What are you working on?"
|
||||||
class="input input-bordered w-full bg-base-300/50 hover:bg-base-300 focus:bg-base-300 border-base-300/50 focus:border-primary transition-colors"
|
class="input input-bordered w-full bg-base-300 hover:bg-base-300 focus:bg-base-300 border-base-content/20 focus:border-primary transition-colors"
|
||||||
:disabled="isRunning"
|
:disabled="isRunning"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,7 +166,7 @@ async function stopTimer() {
|
|||||||
'badge badge-lg cursor-pointer transition-all hover:scale-105',
|
'badge badge-lg cursor-pointer transition-all hover:scale-105',
|
||||||
selectedTagId === tag.id
|
selectedTagId === tag.id
|
||||||
? 'badge-primary shadow-lg shadow-primary/20'
|
? 'badge-primary shadow-lg shadow-primary/20'
|
||||||
: 'badge-outline hover:bg-base-300/50',
|
: 'badge-outline hover:bg-base-300',
|
||||||
]"
|
]"
|
||||||
:disabled="isRunning"
|
:disabled="isRunning"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ function closeShowTokenModal() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
|
<div class="card bg-base-100 shadow-xl border border-base-content/20 mb-6">
|
||||||
<div class="card-body p-4 sm:p-6">
|
<div class="card-body p-4 sm:p-6">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h2 class="card-title text-lg sm:text-xl">
|
<h2 class="card-title text-lg sm:text-xl">
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ async function deletePasskey(id: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
|
<div class="card bg-base-100 shadow-xl border border-base-content/20 mb-6">
|
||||||
<div class="card-body p-4 sm:p-6">
|
<div class="card-body p-4 sm:p-6">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h2 class="card-title text-lg sm:text-xl">
|
<h2 class="card-title text-lg sm:text-xl">
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ async function changePassword() {
|
|||||||
<span>{{ message.text }}</span>
|
<span>{{ message.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
|
<div class="card bg-base-100 shadow-xl border border-base-content/20 mb-6">
|
||||||
<div class="card-body p-4 sm:p-6">
|
<div class="card-body p-4 sm:p-6">
|
||||||
<h2 class="card-title mb-6 text-lg sm:text-xl">
|
<h2 class="card-title mb-6 text-lg sm:text-xl">
|
||||||
<Icon name="key" class="w-5 h-5 sm:w-6 sm:h-6" />
|
<Icon name="key" class="w-5 h-5 sm:w-6 sm:h-6" />
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ async function updateProfile() {
|
|||||||
<span>{{ message.text }}</span>
|
<span>{{ message.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
|
<div class="card bg-base-100 shadow-xl border border-base-content/20 mb-6">
|
||||||
<div class="card-body p-4 sm:p-6">
|
<div class="card-body p-4 sm:p-6">
|
||||||
<h2 class="card-title mb-6 text-lg sm:text-xl">
|
<h2 class="card-title mb-6 text-lg sm:text-xl">
|
||||||
<Icon name="user-circle" class="w-5 h-5 sm:w-6 sm:h-6" />
|
<Icon name="user-circle" class="w-5 h-5 sm:w-6 sm:h-6" />
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ const userMemberships = await db.select({
|
|||||||
.all();
|
.all();
|
||||||
|
|
||||||
const currentTeamId = Astro.cookies.get('currentTeamId')?.value || userMemberships[0]?.organization.id;
|
const currentTeamId = Astro.cookies.get('currentTeamId')?.value || userMemberships[0]?.organization.id;
|
||||||
const currentTeam = userMemberships.find(m => m.organization.id === currentTeamId);
|
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: '/dashboard', label: 'Dashboard', icon: 'home', exact: true },
|
{ href: '/dashboard', label: 'Dashboard', icon: 'home', exact: true },
|
||||||
@@ -54,8 +53,8 @@ function isActive(item: { href: string; exact?: boolean }) {
|
|||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<script>
|
<script is:inline>
|
||||||
const theme = localStorage.getItem('theme') || 'macchiato';
|
const theme = localStorage.getItem('theme') || 'sunset';
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
@@ -64,7 +63,7 @@ function isActive(item: { href: string; exact?: boolean }) {
|
|||||||
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
|
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
|
||||||
<div class="drawer-content flex flex-col h-full overflow-auto">
|
<div class="drawer-content flex flex-col h-full overflow-auto">
|
||||||
<!-- Mobile Navbar -->
|
<!-- Mobile Navbar -->
|
||||||
<div class="navbar bg-base-100 sticky top-0 z-50 lg:hidden border-b border-base-200">
|
<div class="navbar bg-base-100 sticky top-0 z-50 lg:hidden border-b border-base-content/20">
|
||||||
<div class="flex-none">
|
<div class="flex-none">
|
||||||
<label for="my-drawer-2" aria-label="open sidebar" class="btn btn-square btn-ghost btn-sm">
|
<label for="my-drawer-2" aria-label="open sidebar" class="btn btn-square btn-ghost btn-sm">
|
||||||
<Icon name="bars-3" class="w-5 h-5" />
|
<Icon name="bars-3" class="w-5 h-5" />
|
||||||
@@ -87,7 +86,7 @@ function isActive(item: { href: string; exact?: boolean }) {
|
|||||||
|
|
||||||
<div class="drawer-side z-50">
|
<div class="drawer-side z-50">
|
||||||
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
|
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||||
<aside class="bg-base-200 min-h-full w-72 flex flex-col border-r border-base-300/40">
|
<aside class="bg-base-200 min-h-full w-72 flex flex-col border-r border-base-content/20">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<div class="px-5 pt-5 pb-3">
|
<div class="px-5 pt-5 pb-3">
|
||||||
<a href="/dashboard" class="flex items-center gap-2.5 group">
|
<a href="/dashboard" class="flex items-center gap-2.5 group">
|
||||||
@@ -100,7 +99,7 @@ function isActive(item: { href: string; exact?: boolean }) {
|
|||||||
{userMemberships.length > 0 && (
|
{userMemberships.length > 0 && (
|
||||||
<div class="px-4 pb-2">
|
<div class="px-4 pb-2">
|
||||||
<select
|
<select
|
||||||
class="select select-sm w-full bg-base-300/40 border-base-300/60 focus:border-primary/50 focus:outline-none text-sm font-medium"
|
class="select select-sm w-full bg-base-300 border-base-content/20 focus:border-primary focus:outline-none text-sm font-medium"
|
||||||
id="team-switcher"
|
id="team-switcher"
|
||||||
aria-label="Switch team"
|
aria-label="Switch team"
|
||||||
>
|
>
|
||||||
@@ -135,8 +134,8 @@ function isActive(item: { href: string; exact?: boolean }) {
|
|||||||
<a href={item.href} class:list={[
|
<a href={item.href} class:list={[
|
||||||
"rounded-lg gap-3 px-3 py-2.5 font-medium text-sm",
|
"rounded-lg gap-3 px-3 py-2.5 font-medium text-sm",
|
||||||
isActive(item)
|
isActive(item)
|
||||||
? "bg-primary/10 text-primary"
|
? "bg-primary text-primary-content"
|
||||||
: "text-base-content/70 hover:text-base-content hover:bg-base-300/50"
|
: "text-base-content/70 hover:text-base-content hover:bg-base-300"
|
||||||
]}>
|
]}>
|
||||||
<Icon name={item.icon} class="w-[18px] h-[18px]" />
|
<Icon name={item.icon} class="w-[18px] h-[18px]" />
|
||||||
{item.label}
|
{item.label}
|
||||||
@@ -153,8 +152,8 @@ function isActive(item: { href: string; exact?: boolean }) {
|
|||||||
<a href="/admin" class:list={[
|
<a href="/admin" class:list={[
|
||||||
"rounded-lg gap-3 px-3 py-2.5 font-medium text-sm",
|
"rounded-lg gap-3 px-3 py-2.5 font-medium text-sm",
|
||||||
Astro.url.pathname.startsWith("/admin")
|
Astro.url.pathname.startsWith("/admin")
|
||||||
? "bg-primary/10 text-primary"
|
? "bg-primary text-primary-content"
|
||||||
: "text-base-content/70 hover:text-base-content hover:bg-base-300/50"
|
: "text-base-content/70 hover:text-base-content hover:bg-base-300"
|
||||||
]}>
|
]}>
|
||||||
<Icon name="cog-6-tooth" class="w-[18px] h-[18px]" />
|
<Icon name="cog-6-tooth" class="w-[18px] h-[18px]" />
|
||||||
Site Admin
|
Site Admin
|
||||||
@@ -166,25 +165,25 @@ function isActive(item: { href: string; exact?: boolean }) {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Bottom Section -->
|
<!-- Bottom Section -->
|
||||||
<div class="mt-auto border-t border-base-300/40">
|
<div class="mt-auto border-t border-base-content/20">
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<a href="/dashboard/settings" class="flex items-center gap-3 rounded-lg p-2.5 hover:bg-base-300/40 group">
|
<a href="/dashboard/settings" class="flex items-center gap-3 rounded-lg p-2.5 hover:bg-base-300 group">
|
||||||
<Avatar name={user.name} />
|
<Avatar name={user.name} />
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="font-medium text-sm truncate">{user.name}</div>
|
<div class="font-medium text-sm truncate">{user.name}</div>
|
||||||
<div class="text-xs text-base-content/50 truncate">{user.email}</div>
|
<div class="text-xs text-base-content/60 truncate">{user.email}</div>
|
||||||
</div>
|
</div>
|
||||||
<Icon name="chevron-right" class="w-4 h-4 text-base-content/30 group-hover:text-base-content/50" />
|
<Icon name="chevron-right" class="w-4 h-4 text-base-content/50 group-hover:text-base-content/70" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between px-5 pb-2">
|
<div class="flex items-center justify-between px-5 pb-2">
|
||||||
<span class="text-xs text-base-content/40 font-medium">Theme</span>
|
<span class="text-xs text-base-content/60 font-medium">Theme</span>
|
||||||
<ThemeToggle client:load />
|
<ThemeToggle client:load />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-3 pb-3">
|
<div class="px-3 pb-3">
|
||||||
<button id="logout-btn" type="button" class="btn btn-ghost btn-sm btn-block justify-start gap-2 text-base-content/60 hover:text-error hover:bg-error/10 font-medium">
|
<button id="logout-btn" type="button" class="btn btn-ghost btn-sm btn-block justify-start gap-2 text-base-content/60 hover:text-error hover:bg-base-300 font-medium">
|
||||||
<Icon name="arrow-right-on-rectangle" class="w-[18px] h-[18px]" />
|
<Icon name="arrow-right-on-rectangle" class="w-[18px] h-[18px]" />
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ const { title } = Astro.props;
|
|||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<script>
|
<script is:inline>
|
||||||
const theme = localStorage.getItem('theme') || 'macchiato';
|
const theme = localStorage.getItem('theme') || 'sunset';
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const allUsers = await db.select().from(users).all();
|
|||||||
title="Total Users"
|
title="Total Users"
|
||||||
value={String(allUsers.length)}
|
value={String(allUsers.length)}
|
||||||
description="Registered accounts"
|
description="Registered accounts"
|
||||||
icon="heroicons:users"
|
icon="users"
|
||||||
color="text-primary"
|
color="text-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,7 +63,7 @@ const allUsers = await db.select().from(users).all();
|
|||||||
<!-- Users List -->
|
<!-- Users List -->
|
||||||
<div class="card card-border bg-base-100">
|
<div class="card card-border bg-base-100">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="px-4 py-3 border-b border-base-200">
|
<div class="px-4 py-3 border-b border-base-content/20">
|
||||||
<h2 class="text-sm font-semibold">All Users</h2>
|
<h2 class="text-sm font-semibold">All Users</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
@@ -93,7 +93,7 @@ const allUsers = await db.select().from(users).all();
|
|||||||
<span class="badge badge-xs badge-ghost">No</span>
|
<span class="badge badge-xs badge-ghost">No</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-base-content/40">{u.createdAt?.toLocaleDateString() ?? 'N/A'}</td>
|
<td class="text-base-content/60">{u.createdAt?.toLocaleDateString() ?? 'N/A'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const allClients = await db.select()
|
|||||||
<div class="card-body p-4 gap-1">
|
<div class="card-body p-4 gap-1">
|
||||||
<h2 class="font-semibold">{client.name}</h2>
|
<h2 class="font-semibold">{client.name}</h2>
|
||||||
{client.email && <p class="text-sm text-base-content/60">{client.email}</p>}
|
{client.email && <p class="text-sm text-base-content/60">{client.email}</p>}
|
||||||
<p class="text-xs text-base-content/40">Created {client.createdAt?.toLocaleDateString() ?? 'N/A'}</p>
|
<p class="text-xs text-base-content/60">Created {client.createdAt?.toLocaleDateString() ?? 'N/A'}</p>
|
||||||
<div class="card-actions justify-end mt-3">
|
<div class="card-actions justify-end mt-3">
|
||||||
<a href={`/dashboard/clients/${client.id}`} class="btn btn-xs btn-ghost">View</a>
|
<a href={`/dashboard/clients/${client.id}`} class="btn btn-xs btn-ghost">View</a>
|
||||||
<a href={`/dashboard/clients/${client.id}/edit`} class="btn btn-xs btn-primary">Edit</a>
|
<a href={`/dashboard/clients/${client.id}/edit`} class="btn btn-xs btn-primary">Edit</a>
|
||||||
@@ -43,7 +43,7 @@ const allClients = await db.select()
|
|||||||
|
|
||||||
{allClients.length === 0 && (
|
{allClients.length === 0 && (
|
||||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||||
<p class="text-base-content/50 text-sm mb-4">No clients yet</p>
|
<p class="text-base-content/60 text-sm mb-4">No clients yet</p>
|
||||||
<a href="/dashboard/clients/new" class="btn btn-primary btn-sm">Add Your First Client</a>
|
<a href="/dashboard/clients/new" class="btn btn-primary btn-sm">Add Your First Client</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ if (!client) return Astro.redirect('/dashboard/clients');
|
|||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="divider text-xs text-base-content/40">Address Details</div>
|
<div class="divider text-xs text-base-content/60">Address Details</div>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Street Address (optional)</legend>
|
<legend class="fieldset-legend text-xs">Street Address (optional)</legend>
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
|
|||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-base-content/40">Created</div>
|
<div class="text-xs text-base-content/60">Created</div>
|
||||||
<div class="text-sm">{client.createdAt?.toLocaleDateString() ?? 'N/A'}</div>
|
<div class="text-sm">{client.createdAt?.toLocaleDateString() ?? 'N/A'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,7 +160,7 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
|
|||||||
<!-- Recent Activity -->
|
<!-- Recent Activity -->
|
||||||
<div class="card card-border bg-base-100">
|
<div class="card card-border bg-base-100">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="px-4 py-3 border-b border-base-200">
|
<div class="px-4 py-3 border-b border-base-content/20">
|
||||||
<h2 class="text-sm font-semibold">Recent Activity</h2>
|
<h2 class="text-sm font-semibold">Recent Activity</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
|
|||||||
) : '-'}
|
) : '-'}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-base-content/60">{entryUser?.name || 'Unknown'}</td>
|
<td class="text-base-content/60">{entryUser?.name || 'Unknown'}</td>
|
||||||
<td class="text-base-content/40">{entry.startTime.toLocaleDateString()}</td>
|
<td class="text-base-content/60">{entry.startTime.toLocaleDateString()}</td>
|
||||||
<td class="font-mono">{formatTimeRange(entry.startTime, entry.endTime)}</td>
|
<td class="font-mono">{formatTimeRange(entry.startTime, entry.endTime)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -199,13 +199,13 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div class="text-center py-8 text-base-content/40 text-sm">
|
<div class="text-center py-8 text-base-content/60 text-sm">
|
||||||
No time entries recorded for this client yet.
|
No time entries recorded for this client yet.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{recentEntries.length > 0 && (
|
{recentEntries.length > 0 && (
|
||||||
<div class="flex justify-center py-3 border-t border-base-200">
|
<div class="flex justify-center py-3 border-t border-base-content/20">
|
||||||
<a href={`/dashboard/tracker?client=${client.id}`} class="btn btn-ghost btn-xs">
|
<a href={`/dashboard/tracker?client=${client.id}`} class="btn btn-ghost btn-xs">
|
||||||
View All Entries
|
View All Entries
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ if (!user) return Astro.redirect('/login');
|
|||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="divider text-xs text-base-content/40">Address Details</div>
|
<div class="divider text-xs text-base-content/60">Address Details</div>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Street Address (optional)</legend>
|
<legend class="fieldset-legend text-xs">Street Address (optional)</legend>
|
||||||
|
|||||||
@@ -201,9 +201,9 @@ const hasMembership = userOrgs.length > 0;
|
|||||||
{stats.recentEntries.length > 0 ? (
|
{stats.recentEntries.length > 0 ? (
|
||||||
<ul class="space-y-2 mt-3">
|
<ul class="space-y-2 mt-3">
|
||||||
{stats.recentEntries.map(({ entry, client, tag }) => (
|
{stats.recentEntries.map(({ entry, client, tag }) => (
|
||||||
<ColorDot client:load as="li" color={tag?.color || 'oklch(var(--p))'} borderColor class="p-2.5 rounded-lg bg-base-200/50 border-l-3 hover:bg-base-200 transition-colors">
|
<ColorDot client:load as="li" color={tag?.color || 'oklch(var(--p))'} borderColor class="p-2.5 rounded-lg bg-base-200 border-l-3 hover:bg-base-300 transition-colors">
|
||||||
<div class="font-medium text-sm">{client.name}</div>
|
<div class="font-medium text-sm">{client.name}</div>
|
||||||
<div class="text-xs text-base-content/50 mt-0.5 flex flex-wrap gap-2 items-center">
|
<div class="text-xs text-base-content/60 mt-0.5 flex flex-wrap gap-2 items-center">
|
||||||
<span class="flex gap-1 flex-wrap">
|
<span class="flex gap-1 flex-wrap">
|
||||||
{tag ? (
|
{tag ? (
|
||||||
<span class="badge badge-xs badge-outline">{tag.name}</span>
|
<span class="badge badge-xs badge-outline">{tag.name}</span>
|
||||||
@@ -216,8 +216,8 @@ const hasMembership = userOrgs.length > 0;
|
|||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<div class="flex flex-col items-center justify-center py-6 text-center mt-3">
|
<div class="flex flex-col items-center justify-center py-6 text-center mt-3">
|
||||||
<Icon name="clock" class="w-10 h-10 text-base-content/15 mb-2" />
|
<Icon name="clock" class="w-10 h-10 text-base-content/30 mb-2" />
|
||||||
<p class="text-base-content/40 text-sm">No recent time entries</p>
|
<p class="text-base-content/60 text-sm">No recent time entries</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -113,10 +113,10 @@ const isDraft = invoice.status === 'draft';
|
|||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<div role="button" tabindex="0" class="btn btn-square btn-ghost btn-sm border border-base-200">
|
<div role="button" tabindex="0" class="btn btn-square btn-ghost btn-sm border border-base-content/20">
|
||||||
<Icon name="ellipsis-horizontal" class="w-4 h-4" />
|
<Icon name="ellipsis-horizontal" class="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<ul tabindex="0" class="dropdown-content z-1 menu p-2 bg-base-100 rounded-box w-52 border border-base-200">
|
<ul tabindex="0" class="dropdown-content z-1 menu p-2 bg-base-100 rounded-box w-52 border border-base-content/20">
|
||||||
<li>
|
<li>
|
||||||
<a href={`/dashboard/invoices/${invoice.id}/edit`}>
|
<a href={`/dashboard/invoices/${invoice.id}/edit`}>
|
||||||
<Icon name="pencil-square" class="w-4 h-4" />
|
<Icon name="pencil-square" class="w-4 h-4" />
|
||||||
@@ -174,7 +174,7 @@ const isDraft = invoice.status === 'draft';
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<div class="text-4xl font-light text-base-content/30 uppercase tracking-widest mb-4">
|
<div class="text-4xl font-light text-base-content/50 uppercase tracking-widest mb-4">
|
||||||
{invoice.type}
|
{invoice.type}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
|
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
|
||||||
@@ -190,7 +190,7 @@ const isDraft = invoice.status === 'draft';
|
|||||||
|
|
||||||
<!-- Bill To -->
|
<!-- Bill To -->
|
||||||
<div class="mb-12">
|
<div class="mb-12">
|
||||||
<div class="text-xs font-bold uppercase tracking-wider text-base-content/40 mb-2">Bill To</div>
|
<div class="text-xs font-bold uppercase tracking-wider text-base-content/60 mb-2">Bill To</div>
|
||||||
{client ? (
|
{client ? (
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold text-lg">{client.name}</div>
|
<div class="font-bold text-lg">{client.name}</div>
|
||||||
@@ -209,7 +209,7 @@ const isDraft = invoice.status === 'draft';
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div class="italic text-base-content/40">Client deleted</div>
|
<div class="italic text-base-content/60">Client deleted</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ const isDraft = invoice.status === 'draft';
|
|||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full min-w-150">
|
<table class="w-full min-w-150">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b-2 border-base-200 text-left text-xs font-bold uppercase tracking-wider text-base-content/40">
|
<tr class="border-b-2 border-base-content/20 text-left text-xs font-bold uppercase tracking-wider text-base-content/60">
|
||||||
<th class="py-3">Description</th>
|
<th class="py-3">Description</th>
|
||||||
<th class="py-3 text-right w-24">Qty</th>
|
<th class="py-3 text-right w-24">Qty</th>
|
||||||
<th class="py-3 text-right w-32">Price</th>
|
<th class="py-3 text-right w-32">Price</th>
|
||||||
@@ -226,7 +226,7 @@ const isDraft = invoice.status === 'draft';
|
|||||||
{isDraft && <th class="py-3 w-10"></th>}
|
{isDraft && <th class="py-3 w-10"></th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-base-200">
|
<tbody class="divide-y divide-base-content/20">
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-4">{item.description}</td>
|
<td class="py-4">{item.description}</td>
|
||||||
@@ -237,7 +237,7 @@ const isDraft = invoice.status === 'draft';
|
|||||||
<td class="py-4 text-right">
|
<td class="py-4 text-right">
|
||||||
<form method="POST" action={`/api/invoices/${invoice.id}/items/delete`}>
|
<form method="POST" action={`/api/invoices/${invoice.id}/items/delete`}>
|
||||||
<input type="hidden" name="itemId" value={item.id} />
|
<input type="hidden" name="itemId" value={item.id} />
|
||||||
<button type="submit" class="btn btn-ghost btn-xs btn-square text-error opacity-50 hover:opacity-100">
|
<button type="submit" class="btn btn-ghost btn-xs btn-square text-error opacity-70 hover:opacity-100">
|
||||||
<Icon name="trash" class="w-4 h-4" />
|
<Icon name="trash" class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -247,7 +247,7 @@ const isDraft = invoice.status === 'draft';
|
|||||||
))}
|
))}
|
||||||
{items.length === 0 && (
|
{items.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan={isDraft ? 5 : 4} class="py-8 text-center text-base-content/40 italic">
|
<td colspan={isDraft ? 5 : 4} class="py-8 text-center text-base-content/60 italic">
|
||||||
No items added yet.
|
No items added yet.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -266,7 +266,7 @@ const isDraft = invoice.status === 'draft';
|
|||||||
</ModalButton>
|
</ModalButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="POST" action={`/api/invoices/${invoice.id}/items/add`} class="bg-base-200/50 p-4 rounded-lg mb-8 border border-base-200">
|
<form method="POST" action={`/api/invoices/${invoice.id}/items/add`} class="bg-base-200 p-4 rounded-lg mb-8 border border-base-content/20">
|
||||||
<h4 class="text-xs font-semibold mb-3">Add Item</h4>
|
<h4 class="text-xs font-semibold mb-3">Add Item</h4>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-12 gap-3 items-end">
|
<div class="grid grid-cols-1 sm:grid-cols-12 gap-3 items-end">
|
||||||
<div class="sm:col-span-6">
|
<div class="sm:col-span-6">
|
||||||
@@ -329,8 +329,8 @@ const isDraft = invoice.status === 'draft';
|
|||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
{invoice.notes && (
|
{invoice.notes && (
|
||||||
<div class="mt-12 pt-8 border-t border-base-200">
|
<div class="mt-12 pt-8 border-t border-base-content/20">
|
||||||
<div class="text-xs font-bold uppercase tracking-wider text-base-content/40 mb-2">Notes</div>
|
<div class="text-xs font-bold uppercase tracking-wider text-base-content/60 mb-2">Notes</div>
|
||||||
<div class="text-sm whitespace-pre-wrap opacity-80">{invoice.notes}</div>
|
<div class="text-sm whitespace-pre-wrap opacity-80">{invoice.notes}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -211,8 +211,8 @@ const getStatusColor = (status: string) => {
|
|||||||
|
|
||||||
<div class="card card-border bg-base-100">
|
<div class="card card-border bg-base-100">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="px-4 py-3 border-b border-base-200">
|
<div class="px-4 py-3 border-b border-base-content/20">
|
||||||
<p class="text-xs text-base-content/50">
|
<p class="text-xs text-base-content/60">
|
||||||
Showing <span class="font-semibold text-base-content">{allInvoices.length}</span>
|
Showing <span class="font-semibold text-base-content">{allInvoices.length}</span>
|
||||||
{allInvoices.length === 1 ? 'result' : 'results'}
|
{allInvoices.length === 1 ? 'result' : 'results'}
|
||||||
{selectedYear === 'current' ? ` for ${currentYear} (year to date)` : ` for ${selectedYear}`}
|
{selectedYear === 'current' ? ` for ${currentYear} (year to date)` : ` for ${selectedYear}`}
|
||||||
@@ -235,7 +235,7 @@ const getStatusColor = (status: string) => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{allInvoices.length === 0 ? (
|
{allInvoices.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center py-8 text-base-content/50 text-sm">
|
<td colspan="8" class="text-center py-8 text-base-content/60 text-sm">
|
||||||
No invoices or quotes found. Create one to get started.
|
No invoices or quotes found. Create one to get started.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -251,7 +251,7 @@ const getStatusColor = (status: string) => {
|
|||||||
{client ? (
|
{client ? (
|
||||||
<div class="font-medium">{client.name}</div>
|
<div class="font-medium">{client.name}</div>
|
||||||
) : (
|
) : (
|
||||||
<span class="text-base-content/40 italic">Deleted Client</span>
|
<span class="text-base-content/60 italic">Deleted Client</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>{invoice.issueDate.toLocaleDateString()}</td>
|
<td>{invoice.issueDate.toLocaleDateString()}</td>
|
||||||
@@ -272,7 +272,7 @@ const getStatusColor = (status: string) => {
|
|||||||
<div role="button" tabindex="0" class="btn btn-ghost btn-xs btn-square">
|
<div role="button" tabindex="0" class="btn btn-ghost btn-xs btn-square">
|
||||||
<Icon name="ellipsis-vertical" class="w-4 h-4" />
|
<Icon name="ellipsis-vertical" class="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<ul tabindex="0" class="dropdown-content menu p-2 bg-base-100 rounded-box w-52 border border-base-200 z-100">
|
<ul tabindex="0" class="dropdown-content menu p-2 bg-base-100 rounded-box w-52 border border-base-content/20 z-100">
|
||||||
<li>
|
<li>
|
||||||
<a href={`/dashboard/invoices/${invoice.id}`}>
|
<a href={`/dashboard/invoices/${invoice.id}`}>
|
||||||
<Icon name="eye" class="w-4 h-4" />
|
<Icon name="eye" class="w-4 h-4" />
|
||||||
@@ -306,7 +306,7 @@ const getStatusColor = (status: string) => {
|
|||||||
<li>
|
<li>
|
||||||
<ConfirmForm client:load message="Are you sure? This action cannot be undone." action="/api/invoices/delete" class="w-full">
|
<ConfirmForm client:load message="Are you sure? This action cannot be undone." action="/api/invoices/delete" class="w-full">
|
||||||
<input type="hidden" name="id" value={invoice.id} />
|
<input type="hidden" name="id" value={invoice.id} />
|
||||||
<button type="submit" class="w-full justify-start text-error hover:bg-error/10">
|
<button type="submit" class="w-full justify-start text-error hover:bg-base-300">
|
||||||
<Icon name="trash" class="w-4 h-4" />
|
<Icon name="trash" class="w-4 h-4" />
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -104,11 +104,11 @@ const defaultDueDate = nextMonth.toISOString().split('T')[0];
|
|||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Document Type</legend>
|
<legend class="fieldset-legend text-xs">Document Type</legend>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<label class="label cursor-pointer justify-start gap-2 border border-base-200 rounded-lg p-3 flex-1 hover:border-primary has-checked:border-primary has-checked:bg-primary/5 transition-all font-medium text-sm" for="document-type-invoice">
|
<label class="label cursor-pointer justify-start gap-2 border border-base-content/20 rounded-lg p-3 flex-1 hover:border-primary has-checked:border-primary has-checked:bg-base-200 transition-all font-medium text-sm" for="document-type-invoice">
|
||||||
<input type="radio" id="document-type-invoice" name="type" value="invoice" class="radio radio-primary radio-sm" checked />
|
<input type="radio" id="document-type-invoice" name="type" value="invoice" class="radio radio-primary radio-sm" checked />
|
||||||
Invoice
|
Invoice
|
||||||
</label>
|
</label>
|
||||||
<label class="label cursor-pointer justify-start gap-2 border border-base-200 rounded-lg p-3 flex-1 hover:border-primary has-checked:border-primary has-checked:bg-primary/5 transition-all font-medium text-sm" for="document-type-quote">
|
<label class="label cursor-pointer justify-start gap-2 border border-base-content/20 rounded-lg p-3 flex-1 hover:border-primary has-checked:border-primary has-checked:bg-base-200 transition-all font-medium text-sm" for="document-type-quote">
|
||||||
<input type="radio" id="document-type-quote" name="type" value="quote" class="radio radio-primary radio-sm" />
|
<input type="radio" id="document-type-quote" name="type" value="quote" class="radio radio-primary radio-sm" />
|
||||||
Quote / Estimate
|
Quote / Estimate
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -394,11 +394,11 @@ function getTimeRangeLabel(range: string) {
|
|||||||
<div class="stat-title text-xs">Total Invoices</div>
|
<div class="stat-title text-xs">Total Invoices</div>
|
||||||
<div class="stat-value text-2xl">{invoiceStats.total}</div>
|
<div class="stat-value text-2xl">{invoiceStats.total}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat bg-success/10 rounded-lg">
|
<div class="stat bg-base-200 rounded-lg">
|
||||||
<div class="stat-title text-xs">Paid</div>
|
<div class="stat-title text-xs">Paid</div>
|
||||||
<div class="stat-value text-2xl text-success">{invoiceStats.paid}</div>
|
<div class="stat-value text-2xl text-success">{invoiceStats.paid}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat bg-info/10 rounded-lg">
|
<div class="stat bg-base-200 rounded-lg">
|
||||||
<div class="stat-title text-xs">Sent</div>
|
<div class="stat-title text-xs">Sent</div>
|
||||||
<div class="stat-value text-2xl text-info">{invoiceStats.sent}</div>
|
<div class="stat-value text-2xl text-info">{invoiceStats.sent}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -430,15 +430,15 @@ function getTimeRangeLabel(range: string) {
|
|||||||
<div class="stat-title text-xs">Total Quotes</div>
|
<div class="stat-title text-xs">Total Quotes</div>
|
||||||
<div class="stat-value text-2xl">{quoteStats.total}</div>
|
<div class="stat-value text-2xl">{quoteStats.total}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat bg-success/10 rounded-lg">
|
<div class="stat bg-base-200 rounded-lg">
|
||||||
<div class="stat-title text-xs">Accepted</div>
|
<div class="stat-title text-xs">Accepted</div>
|
||||||
<div class="stat-value text-2xl text-success">{quoteStats.accepted}</div>
|
<div class="stat-value text-2xl text-success">{quoteStats.accepted}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat bg-info/10 rounded-lg">
|
<div class="stat bg-base-200 rounded-lg">
|
||||||
<div class="stat-title text-xs">Pending</div>
|
<div class="stat-title text-xs">Pending</div>
|
||||||
<div class="stat-value text-2xl text-info">{quoteStats.sent}</div>
|
<div class="stat-value text-2xl text-info">{quoteStats.sent}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat bg-error/10 rounded-lg">
|
<div class="stat bg-base-200 rounded-lg">
|
||||||
<div class="stat-title text-xs">Declined</div>
|
<div class="stat-title text-xs">Declined</div>
|
||||||
<div class="stat-value text-2xl text-error">{quoteStats.declined}</div>
|
<div class="stat-value text-2xl text-error">{quoteStats.declined}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -591,7 +591,7 @@ function getTimeRangeLabel(range: string) {
|
|||||||
<td>
|
<td>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium">{stat.member.name}</div>
|
<div class="font-medium">{stat.member.name}</div>
|
||||||
<div class="text-xs text-base-content/40">{stat.member.email}</div>
|
<div class="text-xs text-base-content/60">{stat.member.email}</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono text-sm">{formatDuration(stat.totalTime)}</td>
|
<td class="font-mono text-sm">{formatDuration(stat.totalTime)}</td>
|
||||||
@@ -738,7 +738,7 @@ function getTimeRangeLabel(range: string) {
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="whitespace-nowrap">
|
<td class="whitespace-nowrap">
|
||||||
{e.entry.startTime.toLocaleDateString()}<br/>
|
{e.entry.startTime.toLocaleDateString()}<br/>
|
||||||
<span class="text-xs text-base-content/40">
|
<span class="text-xs text-base-content/60">
|
||||||
{e.entry.startTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
|
{e.entry.startTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -753,7 +753,7 @@ function getTimeRangeLabel(range: string) {
|
|||||||
<span>{e.tag.name}</span>
|
<span>{e.tag.name}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span class="text-base-content/30">-</span>
|
<span class="text-base-content/60">-</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-base-content/60">{e.entry.description || '-'}</td>
|
<td class="text-base-content/60">{e.entry.description || '-'}</td>
|
||||||
@@ -770,9 +770,9 @@ function getTimeRangeLabel(range: string) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div class="flex flex-col items-center justify-center py-10 text-center">
|
<div class="flex flex-col items-center justify-center py-10 text-center">
|
||||||
<Icon name="inbox" class="w-12 h-12 text-base-content/15 mb-3" />
|
<Icon name="inbox" class="w-12 h-12 text-base-content/30 mb-3" />
|
||||||
<h3 class="text-base font-semibold mb-1">No time entries found</h3>
|
<h3 class="text-base font-semibold mb-1">No time entries found</h3>
|
||||||
<p class="text-base-content/50 text-sm mb-4">Try adjusting your filters or select a different time range.</p>
|
<p class="text-base-content/60 text-sm mb-4">Try adjusting your filters or select a different time range.</p>
|
||||||
<a href="/dashboard/tracker" class="btn btn-primary btn-sm">
|
<a href="/dashboard/tracker" class="btn btn-primary btn-sm">
|
||||||
<Icon name="play" class="w-4 h-4" />
|
<Icon name="play" class="w-4 h-4" />
|
||||||
Start Tracking Time
|
Start Tracking Time
|
||||||
|
|||||||
@@ -77,11 +77,11 @@ const userPasskeys = await db.select()
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between py-3 border-b border-base-200 gap-2 sm:gap-0">
|
<div class="flex flex-col sm:flex-row sm:justify-between py-3 border-b border-base-content/20 gap-2 sm:gap-0">
|
||||||
<span class="text-base-content/60 text-sm">Account ID</span>
|
<span class="text-base-content/60 text-sm">Account ID</span>
|
||||||
<span class="font-mono text-xs break-all">{user.id}</span>
|
<span class="font-mono text-xs break-all">{user.id}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between py-3 border-b border-base-200 gap-2 sm:gap-0">
|
<div class="flex flex-col sm:flex-row sm:justify-between py-3 border-b border-base-content/20 gap-2 sm:gap-0">
|
||||||
<span class="text-base-content/60 text-sm">Email</span>
|
<span class="text-base-content/60 text-sm">Email</span>
|
||||||
<span class="text-sm break-all">{user.email}</span>
|
<span class="text-sm break-all">{user.email}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const isAdmin = currentUserMember?.member.role === 'owner' || currentUserMember?
|
|||||||
{member.role}
|
{member.role}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-base-content/40">{member.joinedAt?.toLocaleDateString() ?? 'N/A'}</td>
|
<td class="text-base-content/60">{member.joinedAt?.toLocaleDateString() ?? 'N/A'}</td>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<td>
|
<td>
|
||||||
{teamUser.id !== user.id && member.role !== 'owner' && (
|
{teamUser.id !== user.id && member.role !== 'owner' && (
|
||||||
@@ -90,7 +90,7 @@ const isAdmin = currentUserMember?.member.role === 'owner' || currentUserMember?
|
|||||||
<div role="button" tabindex="0" class="btn btn-ghost btn-xs btn-square">
|
<div role="button" tabindex="0" class="btn btn-ghost btn-xs btn-square">
|
||||||
<Icon name="ellipsis-vertical" class="w-4 h-4" />
|
<Icon name="ellipsis-vertical" class="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<ul tabindex="0" class="dropdown-content z-1 menu p-2 bg-base-100 rounded-box w-52 border border-base-200">
|
<ul tabindex="0" class="dropdown-content z-1 menu p-2 bg-base-100 rounded-box w-52 border border-base-content/20">
|
||||||
<li>
|
<li>
|
||||||
<form method="POST" action={`/api/team/change-role`}>
|
<form method="POST" action={`/api/team/change-role`}>
|
||||||
<input type="hidden" name="userId" value={teamUser.id} />
|
<input type="hidden" name="userId" value={teamUser.id} />
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ if (!isAdmin) return Astro.redirect('/dashboard/team');
|
|||||||
<option value="member">Member</option>
|
<option value="member">Member</option>
|
||||||
<option value="admin">Admin</option>
|
<option value="admin">Admin</option>
|
||||||
</select>
|
</select>
|
||||||
<p class="text-xs text-base-content/40 mt-1">Members can track time. Admins can manage team and clients.</p>
|
<p class="text-xs text-base-content/60 mt-1">Members can track time. Admins can manage team and clients.</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="flex justify-end gap-2 mt-4">
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const successType = url.searchParams.get('success');
|
|||||||
<legend class="fieldset-legend text-xs">Team Logo</legend>
|
<legend class="fieldset-legend text-xs">Team Logo</legend>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="avatar placeholder">
|
<div class="avatar placeholder">
|
||||||
<div class="bg-base-200 text-neutral-content rounded-xl w-20 border border-base-200 flex items-center justify-center overflow-hidden">
|
<div class="bg-base-200 text-neutral-content rounded-xl w-20 border border-base-content/20 flex items-center justify-center overflow-hidden">
|
||||||
{organization.logoUrl ? (
|
{organization.logoUrl ? (
|
||||||
<img
|
<img
|
||||||
src={organization.logoUrl}
|
src={organization.logoUrl}
|
||||||
@@ -83,7 +83,7 @@ const successType = url.searchParams.get('success');
|
|||||||
) : (
|
) : (
|
||||||
<Icon
|
<Icon
|
||||||
name="photo"
|
name="photo"
|
||||||
class="w-6 h-6 opacity-40 text-base-content"
|
class="w-6 h-6 opacity-70 text-base-content"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -95,7 +95,7 @@ const successType = url.searchParams.get('success');
|
|||||||
accept="image/png, image/jpeg"
|
accept="image/png, image/jpeg"
|
||||||
class="file-input file-input-bordered file-input-sm w-full max-w-xs"
|
class="file-input file-input-bordered file-input-sm w-full max-w-xs"
|
||||||
/>
|
/>
|
||||||
<div class="text-xs text-base-content/40 mt-1">
|
<div class="text-xs text-base-content/60 mt-1">
|
||||||
Upload a company logo (PNG, JPG). Will be displayed on invoices and quotes.
|
Upload a company logo (PNG, JPG). Will be displayed on invoices and quotes.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,10 +113,10 @@ const successType = url.searchParams.get('success');
|
|||||||
class="input w-full"
|
class="input w-full"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p class="text-xs text-base-content/40 mt-1">This name is visible to all team members</p>
|
<p class="text-xs text-base-content/60 mt-1">This name is visible to all team members</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="divider text-xs text-base-content/40 my-2">Address Information</div>
|
<div class="divider text-xs text-base-content/60 my-2">Address Information</div>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Street Address</legend>
|
<legend class="fieldset-legend text-xs">Street Address</legend>
|
||||||
@@ -182,7 +182,7 @@ const successType = url.searchParams.get('success');
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider text-xs text-base-content/40 my-2">Defaults</div>
|
<div class="divider text-xs text-base-content/60 my-2">Defaults</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
@@ -216,7 +216,7 @@ const successType = url.searchParams.get('success');
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row justify-between items-center gap-3 mt-4">
|
<div class="flex flex-col sm:flex-row justify-between items-center gap-3 mt-4">
|
||||||
<span class="text-xs text-base-content/40 text-center sm:text-left">
|
<span class="text-xs text-base-content/60 text-center sm:text-left">
|
||||||
Address information appears on invoices and quotes
|
Address information appears on invoices and quotes
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -280,7 +280,7 @@ const successType = url.searchParams.get('success');
|
|||||||
{tag.rate ? (
|
{tag.rate ? (
|
||||||
<span class="font-mono text-sm">{new Intl.NumberFormat('en-US', { style: 'currency', currency: organization.defaultCurrency || 'USD' }).format(tag.rate / 100)}</span>
|
<span class="font-mono text-sm">{new Intl.NumberFormat('en-US', { style: 'currency', currency: organization.defaultCurrency || 'USD' }).format(tag.rate / 100)}</span>
|
||||||
) : (
|
) : (
|
||||||
<span class="text-base-content/40 text-xs italic">No rate</span>
|
<span class="text-base-content/60 text-xs italic">No rate</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -315,7 +315,7 @@ const successType = url.searchParams.get('success');
|
|||||||
<fieldset class="fieldset mb-4">
|
<fieldset class="fieldset mb-4">
|
||||||
<legend class="fieldset-legend text-xs">Hourly Rate (cents)</legend>
|
<legend class="fieldset-legend text-xs">Hourly Rate (cents)</legend>
|
||||||
<input type="number" name="rate" value={tag.rate || 0} min="0" class="input w-full" />
|
<input type="number" name="rate" value={tag.rate || 0} min="0" class="input w-full" />
|
||||||
<p class="text-xs text-base-content/40 mt-1">Enter rate in cents (e.g. 5000 = $50.00)</p>
|
<p class="text-xs text-base-content/60 mt-1">Enter rate in cents (e.g. 5000 = $50.00)</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<ModalButton client:load modalId={`edit_tag_modal_${tag.id}`} action="close" class="btn btn-sm">Cancel</ModalButton>
|
<ModalButton client:load modalId={`edit_tag_modal_${tag.id}`} action="close" class="btn btn-sm">Cancel</ModalButton>
|
||||||
@@ -353,7 +353,7 @@ const successType = url.searchParams.get('success');
|
|||||||
<fieldset class="fieldset mb-4">
|
<fieldset class="fieldset mb-4">
|
||||||
<legend class="fieldset-legend text-xs">Hourly Rate (cents)</legend>
|
<legend class="fieldset-legend text-xs">Hourly Rate (cents)</legend>
|
||||||
<input type="number" name="rate" value="0" min="0" class="input w-full" />
|
<input type="number" name="rate" value="0" min="0" class="input w-full" />
|
||||||
<p class="text-xs text-base-content/40 mt-1">Enter rate in cents (e.g. 5000 = $50.00)</p>
|
<p class="text-xs text-base-content/60 mt-1">Enter rate in cents (e.g. 5000 = $50.00)</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<ModalButton client:load modalId="new_tag_modal" action="close" class="btn btn-sm">Cancel</ModalButton>
|
<ModalButton client:load modalId="new_tag_modal" action="close" class="btn btn-sm">Cancel</ModalButton>
|
||||||
|
|||||||
@@ -144,9 +144,9 @@ const paginationPages = getPaginationPages(page, totalPages);
|
|||||||
<h1 class="text-2xl font-extrabold tracking-tight mb-6">Time Tracker</h1>
|
<h1 class="text-2xl font-extrabold tracking-tight mb-6">Time Tracker</h1>
|
||||||
|
|
||||||
<!-- Tabs for Timer and Manual Entry -->
|
<!-- Tabs for Timer and Manual Entry -->
|
||||||
<div class="tabs tabs-lift mb-6">
|
<div class="tabs tabs-border mb-6">
|
||||||
<input type="radio" name="tracker_tabs" class="tab" aria-label="Timer" checked="checked" />
|
<input type="radio" name="tracker_tabs" class="tab" aria-label="Timer" checked="checked" />
|
||||||
<div class="tab-content bg-base-100 border-base-300 p-6">
|
<div class="tab-content border-base-content/20 p-6">
|
||||||
{allClients.length === 0 ? (
|
{allClients.length === 0 ? (
|
||||||
<div class="alert alert-warning flex flex-col sm:flex-row items-center gap-4">
|
<div class="alert alert-warning flex flex-col sm:flex-row items-center gap-4">
|
||||||
<Icon name="exclamation-triangle" class="stroke-current shrink-0 h-6 w-6" />
|
<Icon name="exclamation-triangle" class="stroke-current shrink-0 h-6 w-6" />
|
||||||
@@ -169,7 +169,7 @@ const paginationPages = getPaginationPages(page, totalPages);
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="radio" name="tracker_tabs" class="tab" aria-label="Manual Entry" />
|
<input type="radio" name="tracker_tabs" class="tab" aria-label="Manual Entry" />
|
||||||
<div class="tab-content bg-base-100 border-base-300 p-6">
|
<div class="tab-content border-base-content/20 p-6">
|
||||||
{allClients.length === 0 ? (
|
{allClients.length === 0 ? (
|
||||||
<div class="alert alert-warning flex flex-col sm:flex-row items-center gap-4">
|
<div class="alert alert-warning flex flex-col sm:flex-row items-center gap-4">
|
||||||
<Icon name="exclamation-triangle" class="stroke-current shrink-0 h-6 w-6" />
|
<Icon name="exclamation-triangle" class="stroke-current shrink-0 h-6 w-6" />
|
||||||
@@ -314,7 +314,7 @@ const paginationPages = getPaginationPages(page, totalPages);
|
|||||||
<td>{entryUser?.name || 'Unknown'}</td>
|
<td>{entryUser?.name || 'Unknown'}</td>
|
||||||
<td class="whitespace-nowrap">
|
<td class="whitespace-nowrap">
|
||||||
{entry.startTime.toLocaleDateString()}<br/>
|
{entry.startTime.toLocaleDateString()}<br/>
|
||||||
<span class="text-xs text-base-content/40">
|
<span class="text-xs text-base-content/60">
|
||||||
{entry.startTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
|
{entry.startTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -322,7 +322,7 @@ const paginationPages = getPaginationPages(page, totalPages);
|
|||||||
{entry.endTime ? (
|
{entry.endTime ? (
|
||||||
<>
|
<>
|
||||||
{entry.endTime.toLocaleDateString()}<br/>
|
{entry.endTime.toLocaleDateString()}<br/>
|
||||||
<span class="text-xs text-base-content/40">
|
<span class="text-xs text-base-content/60">
|
||||||
{entry.endTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
|
{entry.endTime.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ if (Astro.locals.user) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Features -->
|
<!-- Features -->
|
||||||
<div class="bg-base-200/50 border-t border-base-200 px-4 py-16 sm:py-20">
|
<div class="bg-base-200 border-t border-base-content/20 px-4 py-16 sm:py-20">
|
||||||
<div class="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div class="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div class="card bg-base-100 card-border">
|
<div class="card bg-base-100 card-border">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-2">
|
<div class="w-10 h-10 rounded-lg bg-base-200 flex items-center justify-center mb-2">
|
||||||
<Icon name="bolt" class="h-5 w-5 text-primary" />
|
<Icon name="bolt" class="h-5 w-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="card-title text-base">Lightning Fast</h3>
|
<h3 class="card-title text-base">Lightning Fast</h3>
|
||||||
@@ -44,7 +44,7 @@ if (Astro.locals.user) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="card bg-base-100 card-border">
|
<div class="card bg-base-100 card-border">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="w-10 h-10 rounded-lg bg-info/10 flex items-center justify-center mb-2">
|
<div class="w-10 h-10 rounded-lg bg-base-200 flex items-center justify-center mb-2">
|
||||||
<Icon name="chart-bar" class="h-5 w-5 text-info" />
|
<Icon name="chart-bar" class="h-5 w-5 text-info" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="card-title text-base">Detailed Reports</h3>
|
<h3 class="card-title text-base">Detailed Reports</h3>
|
||||||
@@ -53,7 +53,7 @@ if (Astro.locals.user) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="card bg-base-100 card-border">
|
<div class="card bg-base-100 card-border">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="w-10 h-10 rounded-lg bg-accent/10 flex items-center justify-center mb-2">
|
<div class="w-10 h-10 rounded-lg bg-base-200 flex items-center justify-center mb-2">
|
||||||
<Icon name="users" class="h-5 w-5 text-accent" />
|
<Icon name="users" class="h-5 w-5 text-accent" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="card-title text-base">Team Collaboration</h3>
|
<h3 class="card-title text-base">Team Collaboration</h3>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const errorMessage =
|
|||||||
|
|
||||||
<Layout title="Login - Chronus">
|
<Layout title="Login - Chronus">
|
||||||
<div class="flex justify-center items-center flex-1 bg-base-100">
|
<div class="flex justify-center items-center flex-1 bg-base-100">
|
||||||
<div class="card card-border bg-base-100 w-full max-w-sm mx-4">
|
<div class="card card-border bg-base-200 w-full max-w-sm mx-4">
|
||||||
<div class="card-body gap-0">
|
<div class="card-body gap-0">
|
||||||
<img src="/logo.webp" alt="Chronus" class="h-14 w-14 mx-auto mb-3" />
|
<img src="/logo.webp" alt="Chronus" class="h-14 w-14 mx-auto mb-3" />
|
||||||
<h2 class="text-2xl font-extrabold tracking-tight text-center">Welcome Back</h2>
|
<h2 class="text-2xl font-extrabold tracking-tight text-center">Welcome Back</h2>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const errorMessage =
|
|||||||
|
|
||||||
<Layout title="Sign Up - Chronus">
|
<Layout title="Sign Up - Chronus">
|
||||||
<div class="flex justify-center items-center flex-1 bg-base-100">
|
<div class="flex justify-center items-center flex-1 bg-base-100">
|
||||||
<div class="card card-border bg-base-100 w-full max-w-sm mx-4">
|
<div class="card card-border bg-base-200 w-full max-w-sm mx-4">
|
||||||
<div class="card-body gap-0">
|
<div class="card-body gap-0">
|
||||||
<img src="/logo.webp" alt="Chronus" class="h-14 w-14 mx-auto mb-3" />
|
<img src="/logo.webp" alt="Chronus" class="h-14 w-14 mx-auto mb-3" />
|
||||||
<h2 class="text-2xl font-extrabold tracking-tight text-center">Create Account</h2>
|
<h2 class="text-2xl font-extrabold tracking-tight text-center">Create Account</h2>
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ export const GET: APIRoute = async ({ params }) => {
|
|||||||
case ".gif":
|
case ".gif":
|
||||||
contentType = "image/gif";
|
contentType = "image/gif";
|
||||||
break;
|
break;
|
||||||
// SVG excluded to prevent stored XSS
|
|
||||||
// WebP omitted — not supported in PDF generation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(fileContent, {
|
return new Response(fileContent, {
|
||||||
|
|||||||
@@ -305,7 +305,6 @@ export function createInvoiceDocument(props: InvoiceDocumentProps) {
|
|||||||
if (organization.logoUrl) {
|
if (organization.logoUrl) {
|
||||||
try {
|
try {
|
||||||
let logoPath;
|
let logoPath;
|
||||||
// Handle uploads directory which might be external to public/
|
|
||||||
if (organization.logoUrl.startsWith("/uploads/")) {
|
if (organization.logoUrl.startsWith("/uploads/")) {
|
||||||
const dataDir = process.env.DATA_DIR
|
const dataDir = process.env.DATA_DIR
|
||||||
? process.env.DATA_DIR
|
? process.env.DATA_DIR
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@plugin "daisyui" {
|
@plugin "daisyui" {
|
||||||
themes: false;
|
themes: sunset --default, winter;
|
||||||
}
|
}
|
||||||
@plugin "./theme-dark.ts";
|
|
||||||
@plugin "./theme-light.ts";
|
|
||||||
|
|
||||||
/* Smoother transitions globally */
|
/* Smoother transitions globally */
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -15,3 +13,8 @@
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Override DaisyUI card-border to use a visible border in both themes */
|
||||||
|
.card-border {
|
||||||
|
border-color: color-mix(in oklab, var(--color-base-content) 20%, transparent);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createCatppuccinPlugin } from "@catppuccin/daisyui";
|
|
||||||
|
|
||||||
export default createCatppuccinPlugin(
|
|
||||||
"macchiato",
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createCatppuccinPlugin } from "@catppuccin/daisyui";
|
|
||||||
|
|
||||||
export default createCatppuccinPlugin(
|
|
||||||
"latte",
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -34,7 +34,6 @@ export async function recalculateInvoiceTotals(invoiceId: string) {
|
|||||||
if (discountType === "percentage") {
|
if (discountType === "percentage") {
|
||||||
discountAmount = Math.round(subtotal * (discountValue / 100));
|
discountAmount = Math.round(subtotal * (discountValue / 100));
|
||||||
} else {
|
} else {
|
||||||
// Fixed amount is assumed to be in cents
|
|
||||||
discountAmount = Math.round(discountValue);
|
discountAmount = Math.round(discountValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user