Icon refactor

This commit is contained in:
2026-02-12 14:29:12 -07:00
parent caf763aa1e
commit 1c70626f5a
36 changed files with 329 additions and 607 deletions

View File

@@ -2,14 +2,12 @@
import { defineConfig } from "astro/config";
import vue from "@astrojs/vue";
import tailwindcss from "@tailwindcss/vite";
import icon from "astro-icon";
import node from "@astrojs/node";
// https://astro.build/config
export default defineConfig({
output: "server",
integrations: [vue(), icon()],
integrations: [vue()],
security: {
csp: process.env.NODE_ENV === "production",
},

View File

@@ -16,13 +16,11 @@
"@astrojs/node": "10.0.0-beta.2",
"@astrojs/vue": "6.0.0-beta.0",
"@ceereals/vue-pdf": "^0.2.1",
"@iconify/vue": "^5.0.0",
"@libsql/client": "^0.17.0",
"@simplewebauthn/browser": "^13.2.2",
"@simplewebauthn/server": "^13.2.2",
"@tailwindcss/vite": "^4.1.18",
"astro": "6.0.0-beta.9",
"astro-icon": "^1.1.5",
"bcryptjs": "^3.0.3",
"chart.js": "^4.5.1",
"daisyui": "^5.5.18",
@@ -36,7 +34,6 @@
},
"devDependencies": {
"@catppuccin/daisyui": "^2.1.1",
"@iconify-json/heroicons": "^1.2.3",
"@react-pdf/types": "^2.9.2",
"drizzle-kit": "0.31.9"
}

442
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

27
src/components/Icon.astro Normal file
View File

@@ -0,0 +1,27 @@
---
import { icons, type IconName } from "../config/icons";
interface Props {
name: IconName;
class?: string;
"class:list"?: any;
}
const { name, class: className, "class:list": classList } = Astro.props;
const svg = icons[name];
if (!svg) {
throw new Error(`Icon "${name}" not found in icon registry`);
}
---
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
class:list={[className, classList]}
aria-hidden="true"
set:html={svg}
/>

30
src/components/Icon.vue Normal file
View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import { computed } from "vue";
import { icons, type IconName } from "../config/icons";
const props = defineProps<{
name: IconName;
class?: string;
}>();
const svg = computed(() => {
const icon = icons[props.name];
if (!icon) {
throw new Error(`Icon "${props.name}" not found in icon registry`);
}
return icon;
});
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
:class="props.class"
aria-hidden="true"
v-html="svg"
/>
</template>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from "vue";
import Icon from "./Icon.vue";
const props = defineProps<{
clients: { id: string; name: string }[];
@@ -164,37 +165,13 @@ function clearForm() {
<!-- Success Message -->
<div v-if="success" class="alert alert-success">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<Icon name="check-circle" class="stroke-current shrink-0 h-6 w-6" />
<span>Manual time entry created successfully!</span>
</div>
<!-- Error Message -->
<div v-if="error" class="alert alert-error">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<Icon name="x-circle" class="stroke-current shrink-0 h-6 w-6" />
<span>{{ error }}</span>
</div>

View File

@@ -1,5 +1,5 @@
---
import { Icon } from 'astro-icon/components';
import Icon from './Icon.astro';
interface Props {
title: string;

View File

@@ -33,7 +33,7 @@ const chartData = computed(() => ({
{
data: props.tags.map((t) => t.totalTime / (1000 * 60)), // Convert to minutes
backgroundColor: props.tags.map((t) => t.color),
borderColor: "#1e293b", // Matches typical dark mode bg
borderColor: "#1e293b",
borderWidth: 2,
},
],

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { Icon } from '@iconify/vue';
import Icon from './Icon.vue';
const theme = ref('macchiato');
@@ -27,7 +27,7 @@ function toggleTheme() {
aria-label="Toggle Theme"
>
<Icon
:icon="theme === 'macchiato' ? 'heroicons:moon' : 'heroicons:sun'"
:name="theme === 'macchiato' ? 'moon' : 'sun'"
class="w-5 h-5"
/>
</button>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { Icon } from "@iconify/vue";
import Icon from "./Icon.vue";
const props = defineProps<{
initialRunningEntry: {
@@ -188,7 +188,7 @@ async function stopTimer() {
@click="startTimer"
class="btn btn-primary btn-lg min-w-40 shadow-lg shadow-primary/20 hover:shadow-xl hover:shadow-primary/30 transition-all"
>
<Icon icon="heroicons:play" class="w-5 h-5" />
<Icon name="play" class="w-5 h-5" />
Start Timer
</button>
<button
@@ -196,7 +196,7 @@ async function stopTimer() {
@click="stopTimer"
class="btn btn-error btn-lg min-w-40 shadow-lg shadow-error/20 hover:shadow-xl hover:shadow-error/30 transition-all"
>
<Icon icon="heroicons:stop" class="w-5 h-5" />
<Icon name="stop" class="w-5 h-5" />
Stop Timer
</button>
</div>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from "vue";
import { Icon } from "@iconify/vue";
import Icon from "../Icon.vue";
import { startAuthentication } from "@simplewebauthn/browser";
const loading = ref(false);
@@ -60,12 +60,12 @@ async function handlePasskeyLogin() {
:disabled="loading"
>
<span v-if="loading" class="loading loading-spinner loading-sm"></span>
<Icon v-else icon="heroicons:finger-print" class="w-5 h-5 mr-2" />
<Icon v-else name="finger-print" class="w-5 h-5 mr-2" />
Sign in with Passkey
</button>
<div v-if="error" role="alert" class="alert alert-error mt-4">
<Icon icon="heroicons:exclamation-circle" class="w-6 h-6" />
<Icon name="exclamation-circle" class="w-6 h-6" />
<span>{{ error }}</span>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { Icon } from "@iconify/vue";
import Icon from "../Icon.vue";
interface ApiToken {
id: string;
@@ -112,7 +112,7 @@ function closeShowTokenModal() {
<div class="flex justify-between items-center mb-6">
<h2 class="card-title text-lg sm:text-xl">
<Icon
icon="heroicons:code-bracket-square"
name="code-bracket-square"
class="w-5 h-5 sm:w-6 sm:h-6"
/>
API Tokens
@@ -121,7 +121,7 @@ function closeShowTokenModal() {
class="btn btn-primary btn-sm"
@click="createModalOpen = true"
>
<Icon icon="heroicons:plus" class="w-4 h-4" />
<Icon name="plus" class="w-4 h-4" />
Create Token
</button>
</div>
@@ -161,7 +161,7 @@ function closeShowTokenModal() {
class="btn btn-ghost btn-xs text-error"
@click="deleteToken(token.id)"
>
<Icon icon="heroicons:trash" class="w-4 h-4" />
<Icon name="trash" class="w-4 h-4" />
</button>
</td>
</tr>
@@ -222,7 +222,7 @@ function closeShowTokenModal() {
<dialog class="modal" :class="{ 'modal-open': showTokenModalOpen }">
<div class="modal-box">
<h3 class="font-bold text-lg text-success flex items-center gap-2">
<Icon icon="heroicons:check-circle" class="w-6 h-6" />
<Icon name="check-circle" class="w-6 h-6" />
Token Created
</h3>
<p class="py-4">
@@ -239,7 +239,7 @@ function closeShowTokenModal() {
@click="copyToken"
title="Copy to clipboard"
>
<Icon icon="heroicons:clipboard" class="w-4 h-4" />
<Icon name="clipboard" class="w-4 h-4" />
</button>
</div>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { Icon } from "@iconify/vue";
import Icon from "../Icon.vue";
import { startRegistration } from "@simplewebauthn/browser";
interface Passkey {
@@ -98,7 +98,7 @@ async function deletePasskey(id: string) {
<div class="card-body p-4 sm:p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="card-title text-lg sm:text-xl">
<Icon icon="heroicons:finger-print" class="w-5 h-5 sm:w-6 sm:h-6" />
<Icon name="finger-print" class="w-5 h-5 sm:w-6 sm:h-6" />
Passkeys
</h2>
<button
@@ -110,7 +110,7 @@ async function deletePasskey(id: string) {
v-if="loading"
class="loading loading-spinner loading-xs"
></span>
<Icon v-else icon="heroicons:plus" class="w-4 h-4" />
<Icon v-else name="plus" class="w-4 h-4" />
Add Passkey
</button>
</div>
@@ -157,7 +157,7 @@ async function deletePasskey(id: string) {
class="btn btn-ghost btn-xs text-error"
@click="deletePasskey(pk.id)"
>
<Icon icon="heroicons:trash" class="w-4 h-4" />
<Icon name="trash" class="w-4 h-4" />
</button>
</td>
</tr>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from "vue";
import { Icon } from "@iconify/vue";
import Icon from "../Icon.vue";
const currentPassword = ref("");
const newPassword = ref("");
@@ -76,10 +76,10 @@ async function changePassword() {
]"
>
<Icon
:icon="
:name="
message.type === 'success'
? 'heroicons:check-circle'
: 'heroicons:exclamation-circle'
? 'check-circle'
: 'exclamation-circle'
"
class="w-6 h-6 shrink-0"
/>
@@ -89,7 +89,7 @@ async function changePassword() {
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
<div class="card-body p-4 sm:p-6">
<h2 class="card-title mb-6 text-lg sm:text-xl">
<Icon icon="heroicons: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" />
Change Password
</h2>
@@ -163,7 +163,7 @@ async function changePassword() {
v-if="loading"
class="loading loading-spinner loading-sm"
></span>
<Icon v-else icon="heroicons:lock-closed" class="w-5 h-5" />
<Icon v-else name="lock-closed" class="w-5 h-5" />
Update Password
</button>
</div>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from "vue";
import { Icon } from "@iconify/vue";
import Icon from "../Icon.vue";
const props = defineProps<{
user: {
@@ -61,10 +61,10 @@ async function updateProfile() {
]"
>
<Icon
:icon="
:name="
message.type === 'success'
? 'heroicons:check-circle'
: 'heroicons:exclamation-circle'
? 'check-circle'
: 'exclamation-circle'
"
class="w-6 h-6 shrink-0"
/>
@@ -74,7 +74,7 @@ async function updateProfile() {
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
<div class="card-body p-4 sm:p-6">
<h2 class="card-title mb-6 text-lg sm:text-xl">
<Icon icon="heroicons: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" />
Profile Information
</h2>
@@ -128,7 +128,7 @@ async function updateProfile() {
v-if="loading"
class="loading loading-spinner loading-sm"
></span>
<Icon v-else icon="heroicons:check" class="w-5 h-5" />
<Icon v-else name="check" class="w-5 h-5" />
Save Changes
</button>
</div>

62
src/config/icons.ts Normal file
View File

@@ -0,0 +1,62 @@
export const icons = {
"arrow-down-tray": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/>`,
"arrow-left": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"/>`,
"arrow-right": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/>`,
"arrow-right-on-rectangle": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9"/>`,
"banknotes": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.25 18.75a60 60 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 0 1 3 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 0 0-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 0 1-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 0 0 3 15h-.75M15 10.5a3 3 0 1 1-6 0a3 3 0 0 1 6 0m3 0h.008v.008H18zm-12 0h.008v.008H6z"/>`,
"bars-3": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>`,
"bolt": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75L12 13.5z"/>`,
"building-office": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21"/>`,
"building-office-2": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008zm0 3h.008v.008h-.008zm0 3h.008v.008h-.008z"/>`,
"calendar": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5"/>`,
"chart-bar": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875zm6.75-4.5c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125zm6.75-4.5c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125z"/>`,
"chart-pie": `<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="M10.5 6a7.5 7.5 0 1 0 7.5 7.5h-7.5z"/><path d="M13.5 10.5H21A7.5 7.5 0 0 0 13.5 3z"/></g>`,
"check": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m4.5 12.75l6 6l9-13.5"/>`,
"check-circle": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12.75L11.25 15L15 9.75M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0"/>`,
"chevron-left": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.75 19.5L8.25 12l7.5-7.5"/>`,
"chevron-right": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m8.25 4.5l7.5 7.5l-7.5 7.5"/>`,
"clipboard": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0q.083.292.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0q.002-.32.084-.612m7.332 0q.969.073 1.927.184c1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48 48 0 0 1 1.927-.184"/>`,
"clipboard-document-list": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48 48 0 0 0-1.123-.08m-5.801 0q-.099.316-.1.664c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75a2.3 2.3 0 0 0-.1-.664m-5.8 0A2.25 2.25 0 0 1 13.5 2.25H15a2.25 2.25 0 0 1 2.15 1.586m-5.8 0q-.563.035-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125zM6.75 12h.008v.008H6.75zm0 3h.008v.008H6.75zm0 3h.008v.008H6.75z"/>`,
"clock": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0a9 9 0 0 1 18 0"/>`,
"code-bracket-square": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25"/>`,
"cog-6-tooth": `<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87q.11.06.22.127c.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a8 8 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a7 7 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a7 7 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a7 7 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124q.108-.066.22-.128c.332-.183.582-.495.644-.869z"/><path d="M15 12a3 3 0 1 1-6 0a3 3 0 0 1 6 0"/></g>`,
"currency-dollar": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0s1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659c-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0"/>`,
"document-currency-dollar": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m3.75 9v7.5m2.25-6.466a9 9 0 0 0-3.461-.203c-.536.072-.974.478-1.021 1.017a5 5 0 0 0-.018.402c0 .464.336.844.775.994l2.95 1.012c.44.15.775.53.775.994q0 .204-.018.402c-.047.539-.485.945-1.021 1.017a9.1 9.1 0 0 1-3.461-.203M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9"/>`,
"document-duplicate": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9 9 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9 9 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"/>`,
"document-text": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9"/>`,
"ellipsis-horizontal": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.75 12a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0m6 0a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0m6 0a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0"/>`,
"ellipsis-vertical": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.75a.75.75 0 1 1 0-1.5a.75.75 0 0 1 0 1.5m0 6a.75.75 0 1 1 0-1.5a.75.75 0 0 1 0 1.5m0 6a.75.75 0 1 1 0-1.5a.75.75 0 0 1 0 1.5"/>`,
"envelope": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"/>`,
"exclamation-circle": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9 3.75h.008v.008H12z"/>`,
"exclamation-triangle": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0zM12 15.75h.007v.008H12z"/>`,
"eye": `<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="M2.036 12.322a1 1 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178c.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178"/><path d="M15 12a3 3 0 1 1-6 0a3 3 0 0 1 6 0"/></g>`,
"finger-print": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7.864 4.243A7.5 7.5 0 0 1 19.5 10.5c0 2.92-.556 5.709-1.568 8.269M5.742 6.364A7.47 7.47 0 0 0 4.5 10.5a7.46 7.46 0 0 1-1.15 3.993m1.989 3.559A11.2 11.2 0 0 0 8.25 10.5a3.75 3.75 0 1 1 7.5 0q0 .79-.064 1.565M12 10.5a14.94 14.94 0 0 1-3.6 9.75m6.633-4.596a18.7 18.7 0 0 1-2.485 5.33"/>`,
"home": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m2.25 12l8.955-8.955a1.124 1.124 0 0 1 1.59 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/>`,
"inbox": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h3.218a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162q0-.338-.1-.661l-2.41-7.839a2.25 2.25 0 0 0-2.15-1.588H6.911a2.25 2.25 0 0 0-2.15 1.588L2.35 13.177a2.3 2.3 0 0 0-.1.661"/>`,
"information-circle": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.25 11.25l.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9-3.75h.008v.008H12z"/>`,
"key": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25"/>`,
"list-bullet": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75zm.375 0a.375.375 0 1 1-.75 0a.375.375 0 0 1 .75 0M3.75 12h.007v.008H3.75zm.375 0a.375.375 0 1 1-.75 0a.375.375 0 0 1 .75 0m-.375 5.25h.007v.008H3.75zm.375 0a.375.375 0 1 1-.75 0a.375.375 0 0 1 .75 0"/>`,
"lock-closed": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25"/>`,
"magnifying-glass": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m21 21l-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607"/>`,
"map-pin": `<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="M15 10.5a3 3 0 1 1-6 0a3 3 0 0 1 6 0"/><path d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0"/></g>`,
"moon": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21.752 15.002A9.7 9.7 0 0 1 18 15.75A9.75 9.75 0 0 1 8.25 6c0-1.33.266-2.597.748-3.752A9.75 9.75 0 0 0 3 11.25A9.75 9.75 0 0 0 12.75 21a9.75 9.75 0 0 0 9.002-5.998"/>`,
"paper-airplane": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 12L3.269 3.125A59.8 59.8 0 0 1 21.486 12a59.8 59.8 0 0 1-18.217 8.875zm0 0h7.5"/>`,
"pencil": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m16.862 4.487l1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8l.8-2.685a4.5 4.5 0 0 1 1.13-1.897zm0 0L19.5 7.125"/>`,
"pencil-square": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m16.862 4.487l1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/>`,
"phone": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 0 0 2.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.04 12.04 0 0 1-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 0 0-1.091-.852H4.5A2.25 2.25 0 0 0 2.25 4.5z"/>`,
"photo": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m2.25 15.75l5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5m10.5-11.25h.008v.008h-.008zm.375 0a.375.375 0 1 1-.75 0a.375.375 0 0 1 .75 0"/>`,
"play": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986z"/>`,
"play-circle": `<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0"/><path d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327z"/></g>`,
"plus": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 4.5v15m7.5-7.5h-15"/>`,
"stop": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5.25 7.5A2.25 2.25 0 0 1 7.5 5.25h9a2.25 2.25 0 0 1 2.25 2.25v9a2.25 2.25 0 0 1-2.25 2.25h-9a2.25 2.25 0 0 1-2.25-2.25z"/>`,
"sun": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0a3.75 3.75 0 0 1 7.5 0"/>`,
"tag": `<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.1 18.1 0 0 0 5.224-5.223c.54-.827.368-1.908-.33-2.607l-9.583-9.58A2.25 2.25 0 0 0 9.568 3"/><path d="M6 6h.008v.008H6z"/></g>`,
"trash": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21q.512.078 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48 48 0 0 0-3.478-.397m-12 .562q.51-.088 1.022-.165m0 0a48 48 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a52 52 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a49 49 0 0 0-7.5 0"/>`,
"user-circle": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17.982 18.725A7.49 7.49 0 0 0 12 15.75a7.49 7.49 0 0 0-5.982 2.975m11.964 0a9 9 0 1 0-11.963 0m11.962 0A8.97 8.97 0 0 1 12 21a8.97 8.97 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0a3 3 0 0 1 6 0"/>`,
"user-group": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M18 18.72a9.1 9.1 0 0 0 3.741-.479q.01-.12.01-.241a3 3 0 0 0-4.692-2.478m.94 3.197l.001.031q0 .337-.037.666A11.94 11.94 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6 6 0 0 1 6 18.719m12 0a5.97 5.97 0 0 0-.941-3.197m0 0A6 6 0 0 0 12 12.75a6 6 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72a9 9 0 0 0 3.74.477m.94-3.197a5.97 5.97 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0a3 3 0 0 1 6 0m6 3a2.25 2.25 0 1 1-4.5 0a2.25 2.25 0 0 1 4.5 0m-13.5 0a2.25 2.25 0 1 1-4.5 0a2.25 2.25 0 0 1 4.5 0"/>`,
"users": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 19.128a9.4 9.4 0 0 0 2.625.372a9.3 9.3 0 0 0 4.121-.952q.004-.086.004-.173a4.125 4.125 0 0 0-7.536-2.32M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.3 12.3 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0a3.375 3.375 0 0 1 6.75 0m8.25 2.25a2.625 2.625 0 1 1-5.25 0a2.625 2.625 0 0 1 5.25 0"/>`,
"x-circle": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0"/>`,
"x-mark": `<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 18L18 6M6 6l12 12"/>`,
} as const;
export type IconName = keyof typeof icons;

View File

@@ -1,6 +1,6 @@
---
import '../styles/global.css';
import { Icon } from 'astro-icon/components';
import Icon from '../components/Icon.astro';
import { db } from '../db';
import { members, organizations } from '../db/schema';
import { eq } from 'drizzle-orm';
@@ -32,12 +32,12 @@ const currentTeamId = Astro.cookies.get('currentTeamId')?.value || userMembershi
const currentTeam = userMemberships.find(m => m.organization.id === currentTeamId);
const navItems = [
{ href: '/dashboard', label: 'Dashboard', icon: 'heroicons:home', exact: true },
{ href: '/dashboard/tracker', label: 'Time Tracker', icon: 'heroicons:clock' },
{ href: '/dashboard/invoices', label: 'Invoices & Quotes', icon: 'heroicons:document-currency-dollar' },
{ href: '/dashboard/reports', label: 'Reports', icon: 'heroicons:chart-bar' },
{ href: '/dashboard/clients', label: 'Clients', icon: 'heroicons:building-office' },
{ href: '/dashboard/team', label: 'Team', icon: 'heroicons:user-group' },
{ href: '/dashboard', label: 'Dashboard', icon: 'home', exact: true },
{ href: '/dashboard/tracker', label: 'Time Tracker', icon: 'clock' },
{ href: '/dashboard/invoices', label: 'Invoices & Quotes', icon: 'document-currency-dollar' },
{ href: '/dashboard/reports', label: 'Reports', icon: 'chart-bar' },
{ href: '/dashboard/clients', label: 'Clients', icon: 'building-office' },
{ href: '/dashboard/team', label: 'Team', icon: 'user-group' },
];
function isActive(item: { href: string; exact?: boolean }) {
@@ -69,7 +69,7 @@ function isActive(item: { href: string; exact?: boolean }) {
<div class="navbar bg-base-100 sticky top-0 z-50 lg:hidden border-b border-base-200">
<div class="flex-none">
<label for="my-drawer-2" aria-label="open sidebar" class="btn btn-square btn-ghost btn-sm">
<Icon name="heroicons:bars-3" class="w-5 h-5" />
<Icon name="bars-3" class="w-5 h-5" />
</label>
</div>
<div class="flex-1 px-2 flex items-center gap-2">
@@ -122,7 +122,7 @@ function isActive(item: { href: string; exact?: boolean }) {
{userMemberships.length === 0 && (
<div class="px-4 pb-2">
<a href="/dashboard/organizations/new" class="btn btn-primary btn-sm btn-block">
<Icon name="heroicons:plus" class="w-4 h-4" />
<Icon name="plus" class="w-4 h-4" />
Create Team
</a>
</div>
@@ -159,7 +159,7 @@ function isActive(item: { href: string; exact?: boolean }) {
? "bg-primary/10 text-primary"
: "text-base-content/70 hover:text-base-content hover:bg-base-300/50"
]}>
<Icon name="heroicons:cog-6-tooth" class="w-[18px] h-[18px]" />
<Icon name="cog-6-tooth" class="w-[18px] h-[18px]" />
Site Admin
</a>
</li>
@@ -177,7 +177,7 @@ function isActive(item: { href: string; exact?: boolean }) {
<div class="font-medium text-sm truncate">{user.name}</div>
<div class="text-xs text-base-content/50 truncate">{user.email}</div>
</div>
<Icon name="heroicons: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/30 group-hover:text-base-content/50" />
</a>
</div>
@@ -189,7 +189,7 @@ function isActive(item: { href: string; exact?: boolean }) {
<div class="px-3 pb-3">
<form action="/api/auth/logout" method="POST">
<button type="submit" 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">
<Icon name="heroicons:arrow-right-on-rectangle" class="w-[18px] h-[18px]" />
<Icon name="arrow-right-on-rectangle" class="w-[18px] h-[18px]" />
Logout
</button>
</form>

View File

@@ -69,7 +69,6 @@ export const GET: APIRoute = async ({ params, locals }) => {
invoice: {
...invoice,
notes: invoice.notes || null,
// Ensure null safety for optional fields that might be undefined in some runtimes depending on driver
discountValue: invoice.discountValue ?? null,
discountType: invoice.discountType ?? null,
discountAmount: invoice.discountAmount ?? null,

View File

@@ -44,7 +44,6 @@ export const POST: APIRoute = async ({ request, params, locals, redirect }) => {
const startDate = new Date(startDateStr);
const endDate = new Date(endDateStr);
// Set end date to end of day
endDate.setHours(23, 59, 59, 999);
const invoice = await db
@@ -174,7 +173,6 @@ export const POST: APIRoute = async ({ request, params, locals, redirect }) => {
for (const day of days.values()) {
const hours = day.totalDuration / (1000 * 60 * 60);
// Avoid division by zero
const unitPrice = hours > 0 ? Math.round(day.totalAmount / hours) : 0;
newItems.push({
@@ -193,7 +191,6 @@ export const POST: APIRoute = async ({ request, params, locals, redirect }) => {
const duration = entry.endTime.getTime() - entry.startTime.getTime();
const hours = duration / (1000 * 60 * 60);
// Determine rate: max of tags, or 0
const rate = rates.length > 0 ? Math.max(...rates) : 0;
const amount = Math.round(hours * rate);

View File

@@ -1,6 +1,6 @@
---
import DashboardLayout from '../../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../../../components/Icon.astro';
import { db } from '../../../../db';
import { clients } from '../../../../db/schema';
import { eq, and } from 'drizzle-orm';
@@ -30,7 +30,7 @@ if (!client) return Astro.redirect('/dashboard/clients');
<div class="max-w-2xl mx-auto">
<div class="flex items-center gap-3 mb-6">
<a href={`/dashboard/clients/${client.id}`} class="btn btn-ghost btn-xs">
<Icon name="heroicons:arrow-left" class="w-4 h-4" />
<Icon name="arrow-left" class="w-4 h-4" />
</a>
<h1 class="text-2xl font-extrabold tracking-tight">Edit Client</h1>
</div>

View File

@@ -1,6 +1,6 @@
---
import DashboardLayout from '../../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../../../components/Icon.astro';
import { db } from '../../../../db';
import { clients, timeEntries, tags, users } from '../../../../db/schema';
import { eq, and, desc, sql } from 'drizzle-orm';
@@ -64,7 +64,7 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
<DashboardLayout title={`${client.name} - Clients - Chronus`}>
<div class="flex items-center gap-3 mb-6">
<a href="/dashboard/clients" class="btn btn-ghost btn-xs">
<Icon name="heroicons:arrow-left" class="w-4 h-4" />
<Icon name="arrow-left" class="w-4 h-4" />
</a>
<h1 class="text-2xl font-extrabold tracking-tight">{client.name}</h1>
</div>
@@ -79,19 +79,19 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
<div class="space-y-2 mb-4">
{client.email && (
<div class="flex items-center gap-2 text-base-content/60 text-sm">
<Icon name="heroicons:envelope" class="w-4 h-4" />
<Icon name="envelope" class="w-4 h-4" />
<a href={`mailto:${client.email}`} class="link link-hover">{client.email}</a>
</div>
)}
{client.phone && (
<div class="flex items-center gap-2 text-base-content/60 text-sm">
<Icon name="heroicons:phone" class="w-4 h-4" />
<Icon name="phone" class="w-4 h-4" />
<a href={`tel:${client.phone}`} class="link link-hover">{client.phone}</a>
</div>
)}
{(client.street || client.city || client.state || client.zip || client.country) && (
<div class="flex items-start gap-2 text-base-content/60">
<Icon name="heroicons:map-pin" class="w-4 h-4 mt-0.5" />
<Icon name="map-pin" class="w-4 h-4 mt-0.5" />
<div class="text-sm space-y-0.5">
{client.street && <div>{client.street}</div>}
{(client.city || client.state || client.zip) && (
@@ -107,12 +107,12 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
</div>
<div class="flex gap-2">
<a href={`/dashboard/clients/${client.id}/edit`} class="btn btn-primary btn-xs">
<Icon name="heroicons:pencil" class="w-3 h-3" />
<Icon name="pencil" class="w-3 h-3" />
Edit
</a>
<form method="POST" action={`/api/clients/${client.id}/delete`} onsubmit="return confirm('Are you sure you want to delete this client? This will also delete all associated time entries.');">
<button type="submit" class="btn btn-error btn-outline btn-xs">
<Icon name="heroicons:trash" class="w-3 h-3" />
<Icon name="trash" class="w-3 h-3" />
Delete
</button>
</form>
@@ -126,14 +126,14 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
title="Total Time Tracked"
value={`${totalHours}h ${totalMinutes}m`}
description="Across all projects"
icon="heroicons:clock"
icon="clock"
color="text-primary"
/>
<StatCard
title="Total Entries"
value={String(totalEntriesCount)}
description="Recorded entries"
icon="heroicons:list-bullet"
icon="list-bullet"
color="text-secondary"
/>
</div>

View File

@@ -1,6 +1,6 @@
---
import DashboardLayout from '../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../components/Icon.astro';
import StatCard from '../../components/StatCard.astro';
import { db } from '../../db';
import { organizations, members, timeEntries, clients, tags } from '../../db/schema';
@@ -112,20 +112,20 @@ const hasMembership = userOrgs.length > 0;
<p class="text-base-content/60 text-sm mt-1">Welcome back, {user.name}!</p>
</div>
<a href="/dashboard/organizations/new" class="btn btn-ghost btn-sm">
<Icon name="heroicons:plus" class="w-4 h-4" />
<Icon name="plus" class="w-4 h-4" />
New Team
</a>
</div>
{!hasMembership && (
<div class="alert alert-info mb-6 text-sm">
<Icon name="heroicons:information-circle" class="w-5 h-5" />
<Icon name="information-circle" class="w-5 h-5" />
<div>
<h3 class="font-bold">Welcome to Chronus!</h3>
<div class="text-xs">You're not part of any team yet. Create one or wait for an invitation.</div>
</div>
<a href="/dashboard/organizations/new" class="btn btn-primary btn-sm">
<Icon name="heroicons:plus" class="w-4 h-4" />
<Icon name="plus" class="w-4 h-4" />
New Team
</a>
</div>
@@ -139,28 +139,28 @@ const hasMembership = userOrgs.length > 0;
title="This Week"
value={formatDuration(stats.totalTimeThisWeek)}
description="Total tracked time"
icon="heroicons:clock"
icon="clock"
color="text-primary"
/>
<StatCard
title="This Month"
value={formatDuration(stats.totalTimeThisMonth)}
description="Total tracked time"
icon="heroicons:calendar"
icon="calendar"
color="text-secondary"
/>
<StatCard
title="Active Timers"
value={String(stats.activeTimers)}
description="Currently running"
icon="heroicons:play-circle"
icon="play-circle"
color="text-accent"
/>
<StatCard
title="Clients"
value={String(stats.totalClients)}
description="Total active"
icon="heroicons:building-office"
icon="building-office"
color="text-info"
/>
</div>
@@ -170,20 +170,20 @@ const hasMembership = userOrgs.length > 0;
<div class="card card-border bg-base-100">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2">
<Icon name="heroicons:bolt" class="w-4 h-4 text-warning" />
<Icon name="bolt" class="w-4 h-4 text-warning" />
Quick Actions
</h2>
<div class="flex flex-col gap-2 mt-3">
<a href="/dashboard/tracker" class="btn btn-primary btn-sm">
<Icon name="heroicons:play" class="w-4 h-4" />
<Icon name="play" class="w-4 h-4" />
Start Timer
</a>
<a href="/dashboard/clients/new" class="btn btn-ghost btn-sm">
<Icon name="heroicons:plus" class="w-4 h-4" />
<Icon name="plus" class="w-4 h-4" />
Add Client
</a>
<a href="/dashboard/reports" class="btn btn-ghost btn-sm">
<Icon name="heroicons:chart-bar" class="w-4 h-4" />
<Icon name="chart-bar" class="w-4 h-4" />
View Reports
</a>
</div>
@@ -194,7 +194,7 @@ const hasMembership = userOrgs.length > 0;
<div class="card card-border bg-base-100">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2">
<Icon name="heroicons:clock" class="w-4 h-4 text-success" />
<Icon name="clock" class="w-4 h-4 text-success" />
Recent Activity
</h2>
{stats.recentEntries.length > 0 ? (
@@ -215,7 +215,7 @@ const hasMembership = userOrgs.length > 0;
</ul>
) : (
<div class="flex flex-col items-center justify-center py-6 text-center mt-3">
<Icon name="heroicons:clock" class="w-10 h-10 text-base-content/15 mb-2" />
<Icon name="clock" class="w-10 h-10 text-base-content/15 mb-2" />
<p class="text-base-content/40 text-sm">No recent time entries</p>
</div>
)}

View File

@@ -1,6 +1,6 @@
---
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../../components/Icon.astro';
import { db } from '../../../db';
import { invoices, invoiceItems, clients, members, organizations } from '../../../db/schema';
import { eq, and } from 'drizzle-orm';
@@ -60,7 +60,7 @@ const isDraft = invoice.status === 'draft';
<div>
<div class="flex items-center gap-2 mb-1">
<a href="/dashboard/invoices" class="btn btn-ghost btn-xs btn-square">
<Icon name="heroicons:arrow-left" class="w-4 h-4" />
<Icon name="arrow-left" class="w-4 h-4" />
</a>
<div class={`badge badge-xs ${
invoice.status === 'paid' || invoice.status === 'accepted' ? 'badge-success' :
@@ -79,7 +79,7 @@ const isDraft = invoice.status === 'draft';
<form method="POST" action={`/api/invoices/${invoice.id}/status`}>
<input type="hidden" name="status" value="sent" />
<button type="submit" class="btn btn-primary btn-sm">
<Icon name="heroicons:paper-airplane" class="w-4 h-4" />
<Icon name="paper-airplane" class="w-4 h-4" />
Mark Sent
</button>
</form>
@@ -88,7 +88,7 @@ const isDraft = invoice.status === 'draft';
<form method="POST" action={`/api/invoices/${invoice.id}/status`}>
<input type="hidden" name="status" value="paid" />
<button type="submit" class="btn btn-success btn-sm">
<Icon name="heroicons:check" class="w-4 h-4" />
<Icon name="check" class="w-4 h-4" />
Mark Paid
</button>
</form>
@@ -97,7 +97,7 @@ const isDraft = invoice.status === 'draft';
<form method="POST" action={`/api/invoices/${invoice.id}/status`}>
<input type="hidden" name="status" value="accepted" />
<button type="submit" class="btn btn-success btn-sm">
<Icon name="heroicons:check" class="w-4 h-4" />
<Icon name="check" class="w-4 h-4" />
Mark Accepted
</button>
</form>
@@ -105,25 +105,25 @@ const isDraft = invoice.status === 'draft';
{(invoice.type === 'quote' && invoice.status === 'accepted') && (
<form method="POST" action={`/api/invoices/${invoice.id}/convert`}>
<button type="submit" class="btn btn-primary btn-sm">
<Icon name="heroicons:document-duplicate" class="w-4 h-4" />
<Icon name="document-duplicate" class="w-4 h-4" />
Convert to Invoice
</button>
</form>
)}
<div class="dropdown dropdown-end">
<div role="button" tabindex="0" class="btn btn-square btn-ghost btn-sm border border-base-200">
<Icon name="heroicons:ellipsis-horizontal" class="w-4 h-4" />
<Icon name="ellipsis-horizontal" class="w-4 h-4" />
</div>
<ul tabindex="0" class="dropdown-content z-1 menu p-2 bg-base-100 rounded-box w-52 border border-base-200">
<li>
<a href={`/dashboard/invoices/${invoice.id}/edit`}>
<Icon name="heroicons:pencil-square" class="w-4 h-4" />
<Icon name="pencil-square" class="w-4 h-4" />
Edit Settings
</a>
</li>
<li>
<a href={`/api/invoices/${invoice.id}/generate`} download>
<Icon name="heroicons:arrow-down-tray" class="w-4 h-4" />
<Icon name="arrow-down-tray" class="w-4 h-4" />
Download PDF
</a>
</li>
@@ -132,7 +132,7 @@ const isDraft = invoice.status === 'draft';
<form method="POST" action={`/api/invoices/${invoice.id}/status`}>
<input type="hidden" name="status" value="void" />
<button type="submit" class="text-error">
<Icon name="heroicons:x-circle" class="w-4 h-4" />
<Icon name="x-circle" class="w-4 h-4" />
Void
</button>
</form>
@@ -142,7 +142,7 @@ const isDraft = invoice.status === 'draft';
<form method="POST" action="/api/invoices/delete" onsubmit="return confirm('Are you sure?');">
<input type="hidden" name="id" value={invoice.id} />
<button type="submit" class="text-error">
<Icon name="heroicons:trash" class="w-4 h-4" />
<Icon name="trash" class="w-4 h-4" />
Delete
</button>
</form>
@@ -236,7 +236,7 @@ const isDraft = invoice.status === 'draft';
<form method="POST" action={`/api/invoices/${invoice.id}/items/delete`}>
<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">
<Icon name="heroicons:trash" class="w-4 h-4" />
<Icon name="trash" class="w-4 h-4" />
</button>
</form>
</td>
@@ -259,7 +259,7 @@ const isDraft = invoice.status === 'draft';
{isDraft && (
<div class="flex justify-end mb-4">
<button onclick="document.getElementById('import_time_modal').showModal()" class="btn btn-sm btn-outline gap-2">
<Icon name="heroicons:clock" class="w-4 h-4" />
<Icon name="clock" class="w-4 h-4" />
Import Time
</button>
</div>
@@ -281,7 +281,7 @@ const isDraft = invoice.status === 'draft';
</div>
<div class="sm:col-span-1">
<button type="submit" class="btn btn-sm btn-primary w-full">
<Icon name="heroicons:plus" class="w-4 h-4" />
<Icon name="plus" class="w-4 h-4" />
</button>
</div>
</div>
@@ -310,7 +310,7 @@ const isDraft = invoice.status === 'draft';
Tax ({invoice.taxRate ?? 0}%)
{isDraft && (
<button type="button" onclick="document.getElementById('tax_modal').showModal()" class="btn btn-ghost btn-xs btn-square opacity-0 group-hover:opacity-100 transition-opacity" title="Edit Tax Rate">
<Icon name="heroicons:pencil" class="w-3 h-3" />
<Icon name="pencil" class="w-3 h-3" />
</button>
)}
</span>

View File

@@ -1,6 +1,6 @@
---
import DashboardLayout from '../../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../../../components/Icon.astro';
import { db } from '../../../../db';
import { invoices, members } from '../../../../db/schema';
import { eq, and } from 'drizzle-orm';
@@ -48,7 +48,7 @@ const discountValueDisplay = invoice.discountType === 'fixed'
<div class="max-w-3xl mx-auto">
<div class="mb-6">
<a href={`/dashboard/invoices/${invoice.id}`} class="btn btn-ghost btn-xs gap-2 pl-0 hover:bg-transparent text-base-content/60">
<Icon name="heroicons:arrow-left" class="w-4 h-4" />
<Icon name="arrow-left" class="w-4 h-4" />
Back to Invoice
</a>
<h1 class="text-2xl font-extrabold tracking-tight mt-2">Edit Details</h1>

View File

@@ -1,6 +1,6 @@
---
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../../components/Icon.astro';
import StatCard from '../../../components/StatCard.astro';
import { db } from '../../../db';
import { invoices, clients } from '../../../db/schema';
@@ -108,7 +108,7 @@ const getStatusColor = (status: string) => {
<p class="text-base-content/60 text-sm mt-1">Manage your billing and estimates</p>
</div>
<a href="/dashboard/invoices/new" class="btn btn-primary btn-sm">
<Icon name="heroicons:plus" class="w-4 h-4" />
<Icon name="plus" class="w-4 h-4" />
Create New
</a>
</div>
@@ -118,14 +118,14 @@ const getStatusColor = (status: string) => {
title="Total Invoices"
value={String(yearInvoices.filter(i => i.invoice.type === 'invoice').length)}
description={selectedYear === 'current' ? `${currentYear} (YTD)` : String(selectedYear)}
icon="heroicons:document-text"
icon="document-text"
color="text-primary"
/>
<StatCard
title="Open Quotes"
value={String(yearInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'sent').length)}
description="Waiting for approval"
icon="heroicons:clipboard-document-list"
icon="clipboard-document-list"
color="text-secondary"
/>
<StatCard
@@ -134,7 +134,7 @@ const getStatusColor = (status: string) => {
.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'paid')
.reduce((acc, curr) => acc + curr.invoice.total, 0), 'USD')}
description={`Paid invoices (${selectedYear === 'current' ? `${currentYear} YTD` : selectedYear})`}
icon="heroicons:currency-dollar"
icon="currency-dollar"
color="text-success"
/>
</div>
@@ -191,7 +191,7 @@ const getStatusColor = (status: string) => {
{(selectedYear !== 'current' || selectedType !== 'all' || selectedStatus !== 'all' || sortBy !== 'date-desc') && (
<div class="mt-3">
<a href="/dashboard/invoices" class="btn btn-ghost btn-xs">
<Icon name="heroicons:x-mark" class="w-3 h-3" />
<Icon name="x-mark" class="w-3 h-3" />
Clear Filters
</a>
</div>
@@ -260,24 +260,24 @@ const getStatusColor = (status: string) => {
<td class="text-right">
<div class="dropdown dropdown-end">
<div role="button" tabindex="0" class="btn btn-ghost btn-xs btn-square">
<Icon name="heroicons:ellipsis-vertical" class="w-4 h-4" />
<Icon name="ellipsis-vertical" class="w-4 h-4" />
</div>
<ul tabindex="0" class="dropdown-content menu p-2 bg-base-100 rounded-box w-52 border border-base-200 z-100">
<li>
<a href={`/dashboard/invoices/${invoice.id}`}>
<Icon name="heroicons:eye" class="w-4 h-4" />
<Icon name="eye" class="w-4 h-4" />
View Details
</a>
</li>
<li>
<a href={`/dashboard/invoices/${invoice.id}/edit`}>
<Icon name="heroicons:pencil-square" class="w-4 h-4" />
<Icon name="pencil-square" class="w-4 h-4" />
Edit
</a>
</li>
<li>
<a href={`/api/invoices/${invoice.id}/generate`} download>
<Icon name="heroicons:arrow-down-tray" class="w-4 h-4" />
<Icon name="arrow-down-tray" class="w-4 h-4" />
Download PDF
</a>
</li>
@@ -286,7 +286,7 @@ const getStatusColor = (status: string) => {
<form method="POST" action={`/api/invoices/${invoice.id}/status`} class="w-full">
<input type="hidden" name="status" value="sent" />
<button type="submit" class="w-full justify-start">
<Icon name="heroicons:paper-airplane" class="w-4 h-4" />
<Icon name="paper-airplane" class="w-4 h-4" />
Mark as Sent
</button>
</form>
@@ -297,7 +297,7 @@ const getStatusColor = (status: string) => {
<form method="POST" action={`/api/invoices/delete`} onsubmit="return confirm('Are you sure? This action cannot be undone.');" class="w-full">
<input type="hidden" name="id" value={invoice.id} />
<button type="submit" class="w-full justify-start text-error hover:bg-error/10">
<Icon name="heroicons:trash" class="w-4 h-4" />
<Icon name="trash" class="w-4 h-4" />
Delete
</button>
</form>

View File

@@ -1,6 +1,6 @@
---
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../../components/Icon.astro';
import { db } from '../../../db';
import { clients, invoices, organizations } from '../../../db/schema';
import { eq, desc, and } from 'drizzle-orm';
@@ -81,7 +81,7 @@ const defaultDueDate = nextMonth.toISOString().split('T')[0];
<div class="max-w-3xl mx-auto">
<div class="mb-6">
<a href="/dashboard/invoices" class="btn btn-ghost btn-xs gap-2 pl-0 hover:bg-transparent text-base-content/60">
<Icon name="heroicons:arrow-left" class="w-4 h-4" />
<Icon name="arrow-left" class="w-4 h-4" />
Back to Invoices
</a>
<h1 class="text-2xl font-extrabold tracking-tight mt-2">Create New Document</h1>
@@ -89,7 +89,7 @@ const defaultDueDate = nextMonth.toISOString().split('T')[0];
{teamClients.length === 0 ? (
<div role="alert" class="alert alert-warning">
<Icon name="heroicons:exclamation-triangle" class="w-5 h-5" />
<Icon name="exclamation-triangle" class="w-5 h-5" />
<div>
<h3 class="font-semibold text-sm">No Clients Found</h3>
<div class="text-xs">You need to add a client before you can create an invoice or quote.</div>
@@ -187,7 +187,7 @@ const defaultDueDate = nextMonth.toISOString().split('T')[0];
<a href="/dashboard/invoices" class="btn btn-ghost btn-sm">Cancel</a>
<button type="submit" class="btn btn-primary btn-sm">
Create Draft
<Icon name="heroicons:arrow-right" class="w-4 h-4" />
<Icon name="arrow-right" class="w-4 h-4" />
</button>
</div>
</div>

View File

@@ -1,6 +1,6 @@
---
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../../components/Icon.astro';
import { db } from '../../../db';
import { organizations, members } from '../../../db/schema';
import { eq } from 'drizzle-orm';
@@ -13,7 +13,7 @@ if (!user) return Astro.redirect('/login');
<div class="max-w-2xl mx-auto">
<div class="flex items-center gap-3 mb-6">
<a href="/dashboard" class="btn btn-ghost btn-xs">
<Icon name="heroicons:arrow-left" class="w-4 h-4" />
<Icon name="arrow-left" class="w-4 h-4" />
</a>
<h1 class="text-2xl font-extrabold tracking-tight">Create New Team</h1>
</div>
@@ -21,7 +21,7 @@ if (!user) return Astro.redirect('/login');
<form method="POST" action="/api/organizations/create" class="card card-border bg-base-100">
<div class="card-body p-4">
<div class="alert alert-info mb-4">
<Icon name="heroicons:information-circle" class="w-4 h-4" />
<Icon name="information-circle" class="w-4 h-4" />
<span class="text-sm">Create a new team to manage separate projects and collaborators. You'll be the owner.</span>
</div>

View File

@@ -1,6 +1,6 @@
---
import DashboardLayout from '../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../components/Icon.astro';
import StatCard from '../../components/StatCard.astro';
import TagChart from '../../components/TagChart.vue';
import ClientChart from '../../components/ClientChart.vue';
@@ -343,28 +343,28 @@ function getTimeRangeLabel(range: string) {
title="Total Time"
value={formatDuration(totalTime)}
description={getTimeRangeLabel(timeRange)}
icon="heroicons:clock"
icon="clock"
color="text-primary"
/>
<StatCard
title="Total Entries"
value={String(entries.length)}
description={getTimeRangeLabel(timeRange)}
icon="heroicons:list-bullet"
icon="list-bullet"
color="text-secondary"
/>
<StatCard
title="Revenue"
value={formatCurrency(revenueStats.total)}
description={`${invoiceStats.paid} paid invoices`}
icon="heroicons:currency-dollar"
icon="currency-dollar"
color="text-success"
/>
<StatCard
title="Active Members"
value={String(statsByMember.filter(s => s.entryCount > 0).length)}
description={`of ${teamMembers.length} total`}
icon="heroicons:user-group"
icon="user-group"
color="text-accent"
/>
</div>
@@ -374,7 +374,7 @@ function getTimeRangeLabel(range: string) {
<div class="card card-border bg-base-100">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:document-text" class="w-4 h-4" />
<Icon name="document-text" class="w-4 h-4" />
Invoices Overview
</h2>
<div class="grid grid-cols-2 gap-4">
@@ -410,7 +410,7 @@ function getTimeRangeLabel(range: string) {
<div class="card card-border bg-base-100">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:clipboard-document-list" class="w-4 h-4" />
<Icon name="clipboard-document-list" class="w-4 h-4" />
Quotes Overview
</h2>
<div class="grid grid-cols-2 gap-4">
@@ -451,7 +451,7 @@ function getTimeRangeLabel(range: string) {
<div class="card card-border bg-base-100 mb-6">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:banknotes" class="w-4 h-4" />
<Icon name="banknotes" class="w-4 h-4" />
Revenue by Client
</h2>
<div class="overflow-x-auto">
@@ -493,7 +493,7 @@ function getTimeRangeLabel(range: string) {
<div class="card card-border bg-base-100">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:chart-pie" class="w-4 h-4" />
<Icon name="chart-pie" class="w-4 h-4" />
Tag Distribution
</h2>
<div class="h-64 w-full">
@@ -515,7 +515,7 @@ function getTimeRangeLabel(range: string) {
<div class="card card-border bg-base-100">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:chart-bar" class="w-4 h-4" />
<Icon name="chart-bar" class="w-4 h-4" />
Time by Client
</h2>
<div class="h-64 w-full">
@@ -537,7 +537,7 @@ function getTimeRangeLabel(range: string) {
<div class="card card-border bg-base-100 mb-6">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:users" class="w-4 h-4" />
<Icon name="users" class="w-4 h-4" />
Time by Team Member
</h2>
<div class="h-64 w-full">
@@ -560,7 +560,7 @@ function getTimeRangeLabel(range: string) {
<div class="card card-border bg-base-100 mb-6">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:users" class="w-4 h-4" />
<Icon name="users" class="w-4 h-4" />
By Team Member
</h2>
<div class="overflow-x-auto">
@@ -601,7 +601,7 @@ function getTimeRangeLabel(range: string) {
<div class="card card-border bg-base-100 mb-6">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:tag" class="w-4 h-4" />
<Icon name="tag" class="w-4 h-4" />
By Tag
</h2>
<div class="overflow-x-auto">
@@ -653,7 +653,7 @@ function getTimeRangeLabel(range: string) {
<div class="card card-border bg-base-100 mb-6">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-3">
<Icon name="heroicons:building-office" class="w-4 h-4" />
<Icon name="building-office" class="w-4 h-4" />
By Client
</h2>
<div class="overflow-x-auto">
@@ -698,12 +698,12 @@ function getTimeRangeLabel(range: string) {
<div class="card-body p-4">
<div class="flex justify-between items-center mb-3">
<h2 class="text-sm font-semibold flex items-center gap-2">
<Icon name="heroicons:document-text" class="w-4 h-4" />
<Icon name="document-text" class="w-4 h-4" />
Detailed Entries ({entries.length})
</h2>
{entries.length > 0 && (
<a href={`/api/reports/export${url.search}`} class="btn btn-xs btn-ghost" target="_blank">
<Icon name="heroicons:arrow-down-tray" class="w-3.5 h-3.5" />
<Icon name="arrow-down-tray" class="w-3.5 h-3.5" />
Export CSV
</a>
)}
@@ -758,11 +758,11 @@ function getTimeRangeLabel(range: string) {
</div>
) : (
<div class="flex flex-col items-center justify-center py-10 text-center">
<Icon name="heroicons:inbox" class="w-12 h-12 text-base-content/15 mb-3" />
<Icon name="inbox" class="w-12 h-12 text-base-content/15 mb-3" />
<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>
<a href="/dashboard/tracker" class="btn btn-primary btn-sm">
<Icon name="heroicons:play" class="w-4 h-4" />
<Icon name="play" class="w-4 h-4" />
Start Tracking Time
</a>
</div>

View File

@@ -1,6 +1,6 @@
---
import DashboardLayout from '../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../components/Icon.astro';
import { db } from '../../db';
import { apiTokens, passkeys } from '../../db/schema';
import { eq, desc } from 'drizzle-orm';
@@ -37,14 +37,14 @@ const userPasskeys = await db.select()
{/* Success Messages */}
{successType === 'profile' && (
<div class="alert alert-success mb-6">
<Icon name="heroicons:check-circle" class="w-5 h-5 sm:w-6 sm:h-6 shrink-0" />
<Icon name="check-circle" class="w-5 h-5 sm:w-6 sm:h-6 shrink-0" />
<span class="text-sm sm:text-base">Profile updated successfully!</span>
</div>
)}
{successType === 'password' && (
<div class="alert alert-success mb-6">
<Icon name="heroicons:check-circle" class="w-5 h-5 sm:w-6 sm:h-6 shrink-0" />
<Icon name="check-circle" class="w-5 h-5 sm:w-6 sm:h-6 shrink-0" />
<span class="text-sm sm:text-base">Password changed successfully!</span>
</div>
)}
@@ -72,7 +72,7 @@ const userPasskeys = await db.select()
<div class="card card-border bg-base-100">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-4">
<Icon name="heroicons:information-circle" class="w-4 h-4" />
<Icon name="information-circle" class="w-4 h-4" />
Account Information
</h2>

View File

@@ -1,7 +1,7 @@
---
import DashboardLayout from '../../layouts/DashboardLayout.astro';
import Avatar from '../../components/Avatar.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../components/Icon.astro';
import { db } from '../../db';
import { members, users } from '../../db/schema';
import { eq } from 'drizzle-orm';
@@ -36,7 +36,7 @@ const isAdmin = currentUserMember?.member.role === 'owner' || currentUserMember?
{isAdmin && (
<>
<a href="/dashboard/team/settings" class="btn btn-ghost btn-sm">
<Icon name="heroicons:cog-6-tooth" class="w-4 h-4" />
<Icon name="cog-6-tooth" class="w-4 h-4" />
Settings
</a>
<a href="/dashboard/team/invite" class="btn btn-primary btn-sm">Invite Member</a>
@@ -88,7 +88,7 @@ const isAdmin = currentUserMember?.member.role === 'owner' || currentUserMember?
{teamUser.id !== user.id && member.role !== 'owner' && (
<div class="dropdown dropdown-end">
<div role="button" tabindex="0" class="btn btn-ghost btn-xs btn-square">
<Icon name="heroicons:ellipsis-vertical" class="w-4 h-4" />
<Icon name="ellipsis-vertical" class="w-4 h-4" />
</div>
<ul tabindex="0" class="dropdown-content z-1 menu p-2 bg-base-100 rounded-box w-52 border border-base-200">
<li>

View File

@@ -1,6 +1,6 @@
---
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../../components/Icon.astro';
import { db } from '../../../db';
import { members } from '../../../db/schema';
import { eq } from 'drizzle-orm';
@@ -34,7 +34,7 @@ if (!isAdmin) return Astro.redirect('/dashboard/team');
<form method="POST" action="/api/team/invite" class="card card-border bg-base-100">
<div class="card-body p-4">
<div class="alert alert-info mb-4">
<Icon name="heroicons:information-circle" class="w-4 h-4 shrink-0" />
<Icon name="information-circle" class="w-4 h-4 shrink-0" />
<span class="text-sm">The user must already have an account. They'll be added to your organization.</span>
</div>

View File

@@ -1,6 +1,6 @@
---
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../../components/Icon.astro';
import { db } from '../../../db';
import { organizations, tags } from '../../../db/schema';
import { eq } from 'drizzle-orm';
@@ -38,7 +38,7 @@ const successType = url.searchParams.get('success');
<DashboardLayout title="Team Settings - Chronus">
<div class="flex items-center gap-3 mb-6">
<a href="/dashboard/team" class="btn btn-ghost btn-xs">
<Icon name="heroicons:arrow-left" class="w-4 h-4" />
<Icon name="arrow-left" class="w-4 h-4" />
</a>
<h1 class="text-2xl font-extrabold tracking-tight">Team Settings</h1>
</div>
@@ -47,13 +47,13 @@ const successType = url.searchParams.get('success');
<div class="card card-border bg-base-100 mb-6">
<div class="card-body p-4">
<h2 class="text-sm font-semibold flex items-center gap-2 mb-4">
<Icon name="heroicons:building-office-2" class="w-4 h-4" />
<Icon name="building-office-2" class="w-4 h-4" />
Team Settings
</h2>
{successType === 'org-name' && (
<div class="alert alert-success mb-4">
<Icon name="heroicons:check-circle" class="w-4 h-4" />
<Icon name="check-circle" class="w-4 h-4" />
<span class="text-sm">Team information updated successfully!</span>
</div>
)}
@@ -79,7 +79,7 @@ const successType = url.searchParams.get('success');
/>
) : (
<Icon
name="heroicons:photo"
name="photo"
class="w-6 h-6 opacity-40 text-base-content"
/>
)}
@@ -218,7 +218,7 @@ const successType = url.searchParams.get('success');
</span>
<button type="submit" class="btn btn-primary btn-sm w-full sm:w-auto">
<Icon name="heroicons:check" class="w-4 h-4" />
<Icon name="check" class="w-4 h-4" />
Save Changes
</button>
</div>
@@ -231,11 +231,11 @@ const successType = url.searchParams.get('success');
<div class="card-body p-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-sm font-semibold flex items-center gap-2">
<Icon name="heroicons:tag" class="w-4 h-4" />
<Icon name="tag" class="w-4 h-4" />
Tags & Rates
</h2>
<button onclick="document.getElementById('new_tag_modal').showModal()" class="btn btn-primary btn-xs">
<Icon name="heroicons:plus" class="w-3 h-3" />
<Icon name="plus" class="w-3 h-3" />
Add Tag
</button>
</div>
@@ -246,7 +246,7 @@ const successType = url.searchParams.get('success');
{allTags.length === 0 ? (
<div class="alert alert-info">
<Icon name="heroicons:information-circle" class="w-4 h-4" />
<Icon name="information-circle" class="w-4 h-4" />
<div>
<div class="font-semibold text-sm">No tags yet</div>
<div class="text-xs">Create tags to add context and rates to your time entries.</div>
@@ -286,11 +286,11 @@ const successType = url.searchParams.get('success');
onclick={`document.getElementById('edit_tag_modal_${tag.id}').showModal()`}
class="btn btn-ghost btn-xs btn-square"
>
<Icon name="heroicons:pencil" class="w-3 h-3" />
<Icon name="pencil" class="w-3 h-3" />
</button>
<form method="POST" action={`/api/tags/${tag.id}/delete`} onsubmit="return confirm('Are you sure you want to delete this tag?');">
<button class="btn btn-ghost btn-xs btn-square text-error">
<Icon name="heroicons:trash" class="w-3 h-3" />
<Icon name="trash" class="w-3 h-3" />
</button>
</form>
</div>

View File

@@ -1,6 +1,6 @@
---
import DashboardLayout from '../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../../components/Icon.astro';
import Timer from '../../components/Timer.vue';
import ManualEntry from '../../components/ManualEntry.vue';
import { db } from '../../db';
@@ -147,7 +147,7 @@ const paginationPages = getPaginationPages(page, totalPages);
<div class="tab-content bg-base-100 border-base-300 p-6">
{allClients.length === 0 ? (
<div class="alert alert-warning flex flex-col sm:flex-row items-center gap-4">
<Icon name="heroicons:exclamation-triangle" class="stroke-current shrink-0 h-6 w-6" />
<Icon name="exclamation-triangle" class="stroke-current shrink-0 h-6 w-6" />
<span class="flex-1 text-center sm:text-left">You need to create a client before tracking time.</span>
<a href="/dashboard/clients/new" class="btn btn-sm btn-primary whitespace-nowrap">Add Client</a>
</div>
@@ -170,7 +170,7 @@ const paginationPages = getPaginationPages(page, totalPages);
<div class="tab-content bg-base-100 border-base-300 p-6">
{allClients.length === 0 ? (
<div class="alert alert-warning flex flex-col sm:flex-row items-center gap-4">
<Icon name="heroicons:exclamation-triangle" class="stroke-current shrink-0 h-6 w-6" />
<Icon name="exclamation-triangle" class="stroke-current shrink-0 h-6 w-6" />
<span class="flex-1 text-center sm:text-left">You need to create a client before adding time entries.</span>
<a href="/dashboard/clients/new" class="btn btn-sm btn-primary whitespace-nowrap">Add Client</a>
</div>
@@ -247,7 +247,7 @@ const paginationPages = getPaginationPages(page, totalPages);
<input type="hidden" name="page" value="1" />
<div class="flex items-end md:col-span-2 lg:col-span-1">
<button type="submit" class="btn btn-primary btn-sm w-full">
<Icon name="heroicons:magnifying-glass" class="w-4 h-4" />
<Icon name="magnifying-glass" class="w-4 h-4" />
Search
</button>
</div>
@@ -259,12 +259,12 @@ const paginationPages = getPaginationPages(page, totalPages);
<div class="card-body p-4">
<div class="flex justify-between items-center mb-3">
<h2 class="text-sm font-semibold flex items-center gap-2">
<Icon name="heroicons:list-bullet" class="w-4 h-4" />
<Icon name="list-bullet" class="w-4 h-4" />
Time Entries ({totalCount?.count || 0} total)
</h2>
{(filterClient || filterStatus || filterType || searchTerm) && (
<a href="/dashboard/tracker" class="btn btn-xs btn-ghost">
<Icon name="heroicons:x-mark" class="w-3 h-3" />
<Icon name="x-mark" class="w-3 h-3" />
Clear Filters
</a>
)}
@@ -289,12 +289,12 @@ const paginationPages = getPaginationPages(page, totalPages);
<td>
{entry.isManual ? (
<span class="badge badge-info badge-xs gap-1" title="Manual Entry">
<Icon name="heroicons:pencil" class="w-3 h-3" />
<Icon name="pencil" class="w-3 h-3" />
Manual
</span>
) : (
<span class="badge badge-success badge-xs gap-1" title="Timed Entry">
<Icon name="heroicons:clock" class="w-3 h-3" />
<Icon name="clock" class="w-3 h-3" />
Timed
</span>
)}
@@ -328,7 +328,7 @@ const paginationPages = getPaginationPages(page, totalPages);
class="btn btn-ghost btn-xs text-error"
onclick="return confirm('Are you sure you want to delete this entry?')"
>
<Icon name="heroicons:trash" class="w-3.5 h-3.5" />
<Icon name="trash" class="w-3.5 h-3.5" />
</button>
</form>
</td>
@@ -345,7 +345,7 @@ const paginationPages = getPaginationPages(page, totalPages);
href={`?page=${Math.max(1, page - 1)}${filterClient ? `&client=${filterClient}` : ''}${filterStatus ? `&status=${filterStatus}` : ''}${filterType ? `&type=${filterType}` : ''}${sortBy ? `&sort=${sortBy}` : ''}${searchTerm ? `&search=${searchTerm}` : ''}`}
class={`btn btn-xs ${page === 1 ? 'btn-disabled' : ''}`}
>
<Icon name="heroicons:chevron-left" class="w-3 h-3" />
<Icon name="chevron-left" class="w-3 h-3" />
Prev
</a>
@@ -365,7 +365,7 @@ const paginationPages = getPaginationPages(page, totalPages);
class={`btn btn-xs ${page === totalPages ? 'btn-disabled' : ''}`}
>
Next
<Icon name="heroicons:chevron-right" class="w-3 h-3" />
<Icon name="chevron-right" class="w-3 h-3" />
</a>
</div>
)}

View File

@@ -1,5 +1,6 @@
---
import Layout from '../layouts/Layout.astro';
import Icon from '../components/Icon.astro';
if (Astro.locals.user) {
return Astro.redirect('/dashboard');
@@ -22,9 +23,7 @@ if (Astro.locals.user) {
<div class="flex gap-3 justify-center flex-wrap">
<a href="/signup" class="btn btn-primary btn-lg px-8">
Get Started
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
<Icon name="arrow-right" class="h-5 w-5" />
</a>
<a href="/login" class="btn btn-ghost btn-lg px-8">Login</a>
</div>
@@ -37,9 +36,7 @@ if (Astro.locals.user) {
<div class="card bg-base-100 card-border">
<div class="card-body">
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd" />
</svg>
<Icon name="bolt" class="h-5 w-5 text-primary" />
</div>
<h3 class="card-title text-base">Lightning Fast</h3>
<p class="text-sm text-base-content/60">Track tasks with a single click. Start, stop, and organize in seconds.</p>
@@ -48,9 +45,7 @@ if (Astro.locals.user) {
<div class="card bg-base-100 card-border">
<div class="card-body">
<div class="w-10 h-10 rounded-lg bg-secondary/10 flex items-center justify-center mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-secondary" viewBox="0 0 20 20" fill="currentColor">
<path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z" />
</svg>
<Icon name="chart-bar" class="h-5 w-5 text-secondary" />
</div>
<h3 class="card-title text-base">Detailed Reports</h3>
<p class="text-sm text-base-content/60">Get actionable insights with charts, filters, and CSV exports.</p>
@@ -59,9 +54,7 @@ if (Astro.locals.user) {
<div class="card bg-base-100 card-border">
<div class="card-body">
<div class="w-10 h-10 rounded-lg bg-accent/10 flex items-center justify-center mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-accent" viewBox="0 0 20 20" fill="currentColor">
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
</svg>
<Icon name="users" class="h-5 w-5 text-accent" />
</div>
<h3 class="card-title text-base">Team Collaboration</h3>
<p class="text-sm text-base-content/60">Built for teams with roles, permissions, and shared workspaces.</p>

View File

@@ -1,6 +1,6 @@
---
import Layout from '../layouts/Layout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../components/Icon.astro';
import PasskeyLogin from '../components/auth/PasskeyLogin.vue';
if (Astro.locals.user) {
@@ -26,7 +26,7 @@ const errorMessage =
{errorMessage && (
<div role="alert" class="alert alert-error mb-4 text-sm">
<Icon name="heroicons:exclamation-circle" class="w-5 h-5" />
<Icon name="exclamation-circle" class="w-5 h-5" />
<span>{errorMessage}</span>
</div>
)}

View File

@@ -1,6 +1,6 @@
---
import Layout from '../layouts/Layout.astro';
import { Icon } from 'astro-icon/components';
import Icon from '../components/Icon.astro';
import { db } from '../db';
import { siteSettings, users } from '../db/schema';
import { eq, count } from 'drizzle-orm';
@@ -42,7 +42,7 @@ const errorMessage =
{errorMessage && (
<div role="alert" class="alert alert-error mb-4 text-sm">
<Icon name="heroicons:exclamation-circle" class="w-5 h-5" />
<Icon name="exclamation-circle" class="w-5 h-5" />
<span>{errorMessage}</span>
</div>
)}
@@ -50,7 +50,7 @@ const errorMessage =
{registrationDisabled ? (
<>
<div class="alert alert-warning text-sm">
<Icon name="heroicons:exclamation-triangle" class="w-5 h-5" />
<Icon name="exclamation-triangle" class="w-5 h-5" />
<span>Registration is currently disabled by the site administrator.</span>
</div>
<div class="divider text-xs"></div>