Strengthened CRF, added more vue, and removed viewtransitions
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m42s
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m42s
This commit is contained in:
@@ -9,7 +9,13 @@ export default defineConfig({
|
|||||||
output: "server",
|
output: "server",
|
||||||
integrations: [vue()],
|
integrations: [vue()],
|
||||||
security: {
|
security: {
|
||||||
csp: process.env.NODE_ENV === "production",
|
csp: process.env.NODE_ENV === "production"
|
||||||
|
? {
|
||||||
|
scriptDirective: {
|
||||||
|
strictDynamic: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: false,
|
||||||
},
|
},
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
|
|||||||
12
src/components/AutoSubmit.vue
Normal file
12
src/components/AutoSubmit.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
function onChange(e: Event) {
|
||||||
|
const el = e.target as HTMLElement;
|
||||||
|
el.closest('form')?.submit();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span @change="onChange">
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
16
src/components/ColorDot.vue
Normal file
16
src/components/ColorDot.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
color: string;
|
||||||
|
as?: string;
|
||||||
|
class?: string;
|
||||||
|
borderColor?: boolean;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="as || 'span'"
|
||||||
|
:class="$props.class"
|
||||||
|
:style="borderColor ? { borderColor: color } : { backgroundColor: color }"
|
||||||
|
><slot /></component>
|
||||||
|
</template>
|
||||||
26
src/components/ConfirmForm.vue
Normal file
26
src/components/ConfirmForm.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
message: string;
|
||||||
|
action: string;
|
||||||
|
method?: string;
|
||||||
|
class?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function onSubmit(e: Event) {
|
||||||
|
if (!confirm((e.currentTarget as HTMLFormElement).dataset.message!)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form
|
||||||
|
:method="method || 'POST'"
|
||||||
|
:action="action"
|
||||||
|
:class="$props.class"
|
||||||
|
:data-message="message"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
34
src/components/ModalButton.vue
Normal file
34
src/components/ModalButton.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
modalId: string;
|
||||||
|
action?: 'open' | 'close';
|
||||||
|
class?: string;
|
||||||
|
title?: string;
|
||||||
|
type?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function onClick(e: MouseEvent) {
|
||||||
|
const btn = e.currentTarget as HTMLElement;
|
||||||
|
const id = btn.dataset.modalId!;
|
||||||
|
const act = btn.dataset.action || 'open';
|
||||||
|
const modal = document.getElementById(id) as HTMLDialogElement | null;
|
||||||
|
if (act === 'close') {
|
||||||
|
modal?.close();
|
||||||
|
} else {
|
||||||
|
modal?.showModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
:type="(type as any) || 'button'"
|
||||||
|
:class="$props.class"
|
||||||
|
:title="$props.title"
|
||||||
|
:data-modal-id="modalId"
|
||||||
|
:data-action="action || 'open'"
|
||||||
|
@click="onClick"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -6,7 +6,6 @@ import { members, organizations } from '../db/schema';
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import Avatar from '../components/Avatar.astro';
|
import Avatar from '../components/Avatar.astro';
|
||||||
import ThemeToggle from '../components/ThemeToggle.vue';
|
import ThemeToggle from '../components/ThemeToggle.vue';
|
||||||
import { ClientRouter } from "astro:transitions";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -55,8 +54,7 @@ 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>
|
||||||
<ClientRouter />
|
<script>
|
||||||
<script is:inline>
|
|
||||||
const theme = localStorage.getItem('theme') || 'macchiato';
|
const theme = localStorage.getItem('theme') || 'macchiato';
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
</script>
|
</script>
|
||||||
@@ -105,7 +103,6 @@ function isActive(item: { href: string; exact?: boolean }) {
|
|||||||
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/40 border-base-300/60 focus:border-primary/50 focus:outline-none text-sm font-medium"
|
||||||
id="team-switcher"
|
id="team-switcher"
|
||||||
aria-label="Switch team"
|
aria-label="Switch team"
|
||||||
onchange="document.cookie = 'currentTeamId=' + this.value + '; path=/'; window.location.reload();"
|
|
||||||
>
|
>
|
||||||
{userMemberships.map(({ membership, organization }) => (
|
{userMemberships.map(({ membership, organization }) => (
|
||||||
<option
|
<option
|
||||||
@@ -187,7 +184,7 @@ function isActive(item: { href: string; exact?: boolean }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-3 pb-3">
|
<div class="px-3 pb-3">
|
||||||
<form action="/api/auth/logout" method="POST" data-astro-reload>
|
<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">
|
<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="arrow-right-on-rectangle" class="w-[18px] h-[18px]" />
|
<Icon name="arrow-right-on-rectangle" class="w-[18px] h-[18px]" />
|
||||||
Logout
|
Logout
|
||||||
@@ -198,5 +195,13 @@ function isActive(item: { href: string; exact?: boolean }) {
|
|||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
// Team switcher - sets cookie and reloads
|
||||||
|
const teamSwitcher = document.getElementById('team-switcher') as HTMLSelectElement | null;
|
||||||
|
teamSwitcher?.addEventListener('change', () => {
|
||||||
|
document.cookie = 'currentTeamId=' + teamSwitcher.value + '; path=/';
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
import { ClientRouter } from "astro:transitions";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -18,8 +17,7 @@ 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>
|
||||||
<ClientRouter />
|
<script>
|
||||||
<script is:inline>
|
|
||||||
const theme = localStorage.getItem('theme') || 'macchiato';
|
const theme = localStorage.getItem('theme') || 'macchiato';
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import DashboardLayout from '../../../../layouts/DashboardLayout.astro';
|
import DashboardLayout from '../../../../layouts/DashboardLayout.astro';
|
||||||
import Icon from '../../../../components/Icon.astro';
|
import Icon from '../../../../components/Icon.astro';
|
||||||
|
import ModalButton from '../../../../components/ModalButton.vue';
|
||||||
import { db } from '../../../../db';
|
import { db } from '../../../../db';
|
||||||
import { clients } from '../../../../db/schema';
|
import { clients } from '../../../../db/schema';
|
||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
@@ -141,13 +142,13 @@ if (!client) return Astro.redirect('/dashboard/clients');
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between items-center mt-4">
|
<div class="flex justify-between items-center mt-4">
|
||||||
<button
|
<ModalButton
|
||||||
type="button"
|
client:load
|
||||||
|
modalId="delete_modal"
|
||||||
class="btn btn-error btn-outline btn-sm"
|
class="btn btn-error btn-outline btn-sm"
|
||||||
onclick={`document.getElementById('delete_modal').showModal()`}
|
|
||||||
>
|
>
|
||||||
Delete Client
|
Delete Client
|
||||||
</button>
|
</ModalButton>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<a href={`/dashboard/clients/${client.id}`} class="btn btn-ghost btn-sm">Cancel</a>
|
<a href={`/dashboard/clients/${client.id}`} class="btn btn-ghost btn-sm">Cancel</a>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
import DashboardLayout from '../../../../layouts/DashboardLayout.astro';
|
import DashboardLayout from '../../../../layouts/DashboardLayout.astro';
|
||||||
import Icon from '../../../../components/Icon.astro';
|
import Icon from '../../../../components/Icon.astro';
|
||||||
|
import ConfirmForm from '../../../../components/ConfirmForm.vue';
|
||||||
|
import ColorDot from '../../../../components/ColorDot.vue';
|
||||||
import { db } from '../../../../db';
|
import { db } from '../../../../db';
|
||||||
import { clients, timeEntries, tags, users } from '../../../../db/schema';
|
import { clients, timeEntries, tags, users } from '../../../../db/schema';
|
||||||
import { eq, and, desc, sql } from 'drizzle-orm';
|
import { eq, and, desc, sql } from 'drizzle-orm';
|
||||||
@@ -110,12 +112,12 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
|
|||||||
<Icon name="pencil" class="w-3 h-3" />
|
<Icon name="pencil" class="w-3 h-3" />
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</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.');">
|
<ConfirmForm client:load message="Are you sure you want to delete this client? This will also delete all associated time entries." action={`/api/clients/${client.id}/delete`}>
|
||||||
<button type="submit" class="btn btn-error btn-outline btn-xs">
|
<button type="submit" class="btn btn-error btn-outline btn-xs">
|
||||||
<Icon name="trash" class="w-3 h-3" />
|
<Icon name="trash" class="w-3 h-3" />
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</ConfirmForm>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -182,7 +184,7 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
|
|||||||
{tag ? (
|
{tag ? (
|
||||||
<div class="badge badge-xs badge-outline flex items-center gap-1">
|
<div class="badge badge-xs badge-outline flex items-center gap-1">
|
||||||
{tag.color && (
|
{tag.color && (
|
||||||
<span class="w-2 h-2 rounded-full" style={`background-color: ${tag.color}`}></span>
|
<ColorDot client:load color={tag.color} class="w-2 h-2 rounded-full" />
|
||||||
)}
|
)}
|
||||||
<span>{tag.name}</span>
|
<span>{tag.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import DashboardLayout from '../../layouts/DashboardLayout.astro';
|
import DashboardLayout from '../../layouts/DashboardLayout.astro';
|
||||||
import Icon from '../../components/Icon.astro';
|
import Icon from '../../components/Icon.astro';
|
||||||
import StatCard from '../../components/StatCard.astro';
|
import StatCard from '../../components/StatCard.astro';
|
||||||
|
import ColorDot from '../../components/ColorDot.vue';
|
||||||
import { db } from '../../db';
|
import { db } from '../../db';
|
||||||
import { organizations, members, timeEntries, clients, tags } from '../../db/schema';
|
import { organizations, members, timeEntries, clients, tags } from '../../db/schema';
|
||||||
import { eq, desc, and, isNull, gte, sql } from 'drizzle-orm';
|
import { eq, desc, and, isNull, gte, sql } from 'drizzle-orm';
|
||||||
@@ -200,7 +201,7 @@ 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 }) => (
|
||||||
<li class="p-2.5 rounded-lg bg-base-200/50 border-l-3 hover:bg-base-200 transition-colors" style={`border-color: ${tag?.color || 'oklch(var(--p))'}`}>
|
<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">
|
||||||
<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/50 mt-0.5 flex flex-wrap gap-2 items-center">
|
||||||
<span class="flex gap-1 flex-wrap">
|
<span class="flex gap-1 flex-wrap">
|
||||||
@@ -210,7 +211,7 @@ const hasMembership = userOrgs.length > 0;
|
|||||||
</span>
|
</span>
|
||||||
<span>· {entry.endTime ? formatDuration(entry.endTime.getTime() - entry.startTime.getTime()) : 'Running...'}</span>
|
<span>· {entry.endTime ? formatDuration(entry.endTime.getTime() - entry.startTime.getTime()) : 'Running...'}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</ColorDot>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
|
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
|
||||||
import Icon from '../../../components/Icon.astro';
|
import Icon from '../../../components/Icon.astro';
|
||||||
|
import ConfirmForm from '../../../components/ConfirmForm.vue';
|
||||||
|
import ModalButton from '../../../components/ModalButton.vue';
|
||||||
import { db } from '../../../db';
|
import { db } from '../../../db';
|
||||||
import { invoices, invoiceItems, clients, members, organizations } from '../../../db/schema';
|
import { invoices, invoiceItems, clients, members, organizations } from '../../../db/schema';
|
||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
@@ -139,13 +141,13 @@ const isDraft = invoice.status === 'draft';
|
|||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
<li>
|
<li>
|
||||||
<form method="POST" action="/api/invoices/delete" onsubmit="return confirm('Are you sure?');">
|
<ConfirmForm client:load message="Are you sure?" action="/api/invoices/delete">
|
||||||
<input type="hidden" name="id" value={invoice.id} />
|
<input type="hidden" name="id" value={invoice.id} />
|
||||||
<button type="submit" class="text-error">
|
<button type="submit" class="text-error">
|
||||||
<Icon name="trash" class="w-4 h-4" />
|
<Icon name="trash" class="w-4 h-4" />
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</ConfirmForm>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,10 +260,10 @@ const isDraft = invoice.status === 'draft';
|
|||||||
<!-- Add Item Form (Only if Draft) -->
|
<!-- Add Item Form (Only if Draft) -->
|
||||||
{isDraft && (
|
{isDraft && (
|
||||||
<div class="flex justify-end mb-4">
|
<div class="flex justify-end mb-4">
|
||||||
<button onclick="document.getElementById('import_time_modal').showModal()" class="btn btn-sm btn-outline gap-2">
|
<ModalButton client:load modalId="import_time_modal" class="btn btn-sm btn-outline gap-2">
|
||||||
<Icon name="clock" class="w-4 h-4" />
|
<Icon name="clock" class="w-4 h-4" />
|
||||||
Import Time
|
Import Time
|
||||||
</button>
|
</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/50 p-4 rounded-lg mb-8 border border-base-200">
|
||||||
@@ -309,9 +311,9 @@ const isDraft = invoice.status === 'draft';
|
|||||||
<span class="text-base-content/60 flex items-center gap-2">
|
<span class="text-base-content/60 flex items-center gap-2">
|
||||||
Tax ({invoice.taxRate ?? 0}%)
|
Tax ({invoice.taxRate ?? 0}%)
|
||||||
{isDraft && (
|
{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">
|
<ModalButton client:load modalId="tax_modal" class="btn btn-ghost btn-xs btn-square opacity-0 group-hover:opacity-100 transition-opacity" title="Edit Tax Rate">
|
||||||
<Icon name="pencil" class="w-3 h-3" />
|
<Icon name="pencil" class="w-3 h-3" />
|
||||||
</button>
|
</ModalButton>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span class="font-medium">{formatCurrency(invoice.taxAmount, invoice.currency)}</span>
|
<span class="font-medium">{formatCurrency(invoice.taxAmount, invoice.currency)}</span>
|
||||||
@@ -364,7 +366,7 @@ const isDraft = invoice.status === 'draft';
|
|||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<button type="button" class="btn btn-sm" onclick="document.getElementById('tax_modal').close()">Cancel</button>
|
<ModalButton client:load modalId="tax_modal" action="close" class="btn btn-sm">Cancel</ModalButton>
|
||||||
<button type="submit" class="btn btn-primary btn-sm">Update</button>
|
<button type="submit" class="btn btn-primary btn-sm">Update</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -397,7 +399,7 @@ const isDraft = invoice.status === 'draft';
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<button type="button" class="btn btn-sm" onclick="document.getElementById('import_time_modal').close()">Cancel</button>
|
<ModalButton client:load modalId="import_time_modal" action="close" class="btn btn-sm">Cancel</ModalButton>
|
||||||
<button type="submit" class="btn btn-primary btn-sm">Import</button>
|
<button type="submit" class="btn btn-primary btn-sm">Import</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
|
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
|
||||||
import Icon from '../../../components/Icon.astro';
|
import Icon from '../../../components/Icon.astro';
|
||||||
import StatCard from '../../../components/StatCard.astro';
|
import StatCard from '../../../components/StatCard.astro';
|
||||||
|
import AutoSubmit from '../../../components/AutoSubmit.vue';
|
||||||
|
import ConfirmForm from '../../../components/ConfirmForm.vue';
|
||||||
import { db } from '../../../db';
|
import { db } from '../../../db';
|
||||||
import { invoices, clients } from '../../../db/schema';
|
import { invoices, clients } from '../../../db/schema';
|
||||||
import { eq, desc, and, gte, lte, sql } from 'drizzle-orm';
|
import { eq, desc, and, gte, lte, sql } from 'drizzle-orm';
|
||||||
@@ -145,46 +147,54 @@ const getStatusColor = (status: string) => {
|
|||||||
<form method="GET" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
<form method="GET" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Year</legend>
|
<legend class="fieldset-legend text-xs">Year</legend>
|
||||||
<select name="year" class="select w-full" onchange="this.form.submit()">
|
<AutoSubmit client:load>
|
||||||
<option value="current" selected={selectedYear === 'current'}>Current Year to Date ({currentYear})</option>
|
<select name="year" class="select w-full">
|
||||||
{availableYears.map(year => (
|
<option value="current" selected={selectedYear === 'current'}>Current Year to Date ({currentYear})</option>
|
||||||
<option value={year} selected={year === selectedYear}>{year}</option>
|
{availableYears.map(year => (
|
||||||
))}
|
<option value={year} selected={year === selectedYear}>{year}</option>
|
||||||
</select>
|
))}
|
||||||
|
</select>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Type</legend>
|
<legend class="fieldset-legend text-xs">Type</legend>
|
||||||
<select name="type" class="select w-full" onchange="this.form.submit()">
|
<AutoSubmit client:load>
|
||||||
<option value="all" selected={selectedType === 'all'}>All Types</option>
|
<select name="type" class="select w-full">
|
||||||
<option value="invoice" selected={selectedType === 'invoice'}>Invoices</option>
|
<option value="all" selected={selectedType === 'all'}>All Types</option>
|
||||||
<option value="quote" selected={selectedType === 'quote'}>Quotes</option>
|
<option value="invoice" selected={selectedType === 'invoice'}>Invoices</option>
|
||||||
</select>
|
<option value="quote" selected={selectedType === 'quote'}>Quotes</option>
|
||||||
|
</select>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Status</legend>
|
<legend class="fieldset-legend text-xs">Status</legend>
|
||||||
<select name="status" class="select w-full" onchange="this.form.submit()">
|
<AutoSubmit client:load>
|
||||||
<option value="all" selected={selectedStatus === 'all'}>All Statuses</option>
|
<select name="status" class="select w-full">
|
||||||
<option value="draft" selected={selectedStatus === 'draft'}>Draft</option>
|
<option value="all" selected={selectedStatus === 'all'}>All Statuses</option>
|
||||||
<option value="sent" selected={selectedStatus === 'sent'}>Sent</option>
|
<option value="draft" selected={selectedStatus === 'draft'}>Draft</option>
|
||||||
<option value="paid" selected={selectedStatus === 'paid'}>Paid</option>
|
<option value="sent" selected={selectedStatus === 'sent'}>Sent</option>
|
||||||
<option value="accepted" selected={selectedStatus === 'accepted'}>Accepted</option>
|
<option value="paid" selected={selectedStatus === 'paid'}>Paid</option>
|
||||||
<option value="declined" selected={selectedStatus === 'declined'}>Declined</option>
|
<option value="accepted" selected={selectedStatus === 'accepted'}>Accepted</option>
|
||||||
<option value="void" selected={selectedStatus === 'void'}>Void</option>
|
<option value="declined" selected={selectedStatus === 'declined'}>Declined</option>
|
||||||
</select>
|
<option value="void" selected={selectedStatus === 'void'}>Void</option>
|
||||||
|
</select>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Sort By</legend>
|
<legend class="fieldset-legend text-xs">Sort By</legend>
|
||||||
<select name="sort" class="select w-full" onchange="this.form.submit()">
|
<AutoSubmit client:load>
|
||||||
<option value="date-desc" selected={sortBy === 'date-desc'}>Date (Newest First)</option>
|
<select name="sort" class="select w-full">
|
||||||
<option value="date-asc" selected={sortBy === 'date-asc'}>Date (Oldest First)</option>
|
<option value="date-desc" selected={sortBy === 'date-desc'}>Date (Newest First)</option>
|
||||||
<option value="amount-desc" selected={sortBy === 'amount-desc'}>Amount (High to Low)</option>
|
<option value="date-asc" selected={sortBy === 'date-asc'}>Date (Oldest First)</option>
|
||||||
<option value="amount-asc" selected={sortBy === 'amount-asc'}>Amount (Low to High)</option>
|
<option value="amount-desc" selected={sortBy === 'amount-desc'}>Amount (High to Low)</option>
|
||||||
<option value="number-desc" selected={sortBy === 'number-desc'}>Number (Z-A)</option>
|
<option value="amount-asc" selected={sortBy === 'amount-asc'}>Amount (Low to High)</option>
|
||||||
<option value="number-asc" selected={sortBy === 'number-asc'}>Number (A-Z)</option>
|
<option value="number-desc" selected={sortBy === 'number-desc'}>Number (Z-A)</option>
|
||||||
</select>
|
<option value="number-asc" selected={sortBy === 'number-asc'}>Number (A-Z)</option>
|
||||||
|
</select>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -294,13 +304,13 @@ const getStatusColor = (status: string) => {
|
|||||||
)}
|
)}
|
||||||
<div class="divider my-1"></div>
|
<div class="divider my-1"></div>
|
||||||
<li>
|
<li>
|
||||||
<form method="POST" action={`/api/invoices/delete`} onsubmit="return confirm('Are you sure? This action cannot be undone.');" 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-error/10">
|
||||||
<Icon name="trash" class="w-4 h-4" />
|
<Icon name="trash" class="w-4 h-4" />
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</ConfirmForm>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import StatCard from '../../components/StatCard.astro';
|
|||||||
import TagChart from '../../components/TagChart.vue';
|
import TagChart from '../../components/TagChart.vue';
|
||||||
import ClientChart from '../../components/ClientChart.vue';
|
import ClientChart from '../../components/ClientChart.vue';
|
||||||
import MemberChart from '../../components/MemberChart.vue';
|
import MemberChart from '../../components/MemberChart.vue';
|
||||||
|
import AutoSubmit from '../../components/AutoSubmit.vue';
|
||||||
|
import ColorDot from '../../components/ColorDot.vue';
|
||||||
import { db } from '../../db';
|
import { db } from '../../db';
|
||||||
import { timeEntries, members, users, clients, tags, invoices } from '../../db/schema';
|
import { timeEntries, members, users, clients, tags, invoices } from '../../db/schema';
|
||||||
import { eq, and, gte, lte, sql, desc } from 'drizzle-orm';
|
import { eq, and, gte, lte, sql, desc } from 'drizzle-orm';
|
||||||
@@ -260,78 +262,88 @@ function getTimeRangeLabel(range: string) {
|
|||||||
<form method="GET" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
<form method="GET" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Time Range</legend>
|
<legend class="fieldset-legend text-xs">Time Range</legend>
|
||||||
<select id="reports-range" name="range" class="select w-full" onchange="this.form.submit()">
|
<AutoSubmit client:load>
|
||||||
<option value="today" selected={timeRange === 'today'}>Today</option>
|
<select id="reports-range" name="range" class="select w-full">
|
||||||
<option value="week" selected={timeRange === 'week'}>Last 7 Days</option>
|
<option value="today" selected={timeRange === 'today'}>Today</option>
|
||||||
<option value="month" selected={timeRange === 'month'}>Last 30 Days</option>
|
<option value="week" selected={timeRange === 'week'}>Last 7 Days</option>
|
||||||
<option value="mtd" selected={timeRange === 'mtd'}>Month to Date</option>
|
<option value="month" selected={timeRange === 'month'}>Last 30 Days</option>
|
||||||
<option value="ytd" selected={timeRange === 'ytd'}>Year to Date</option>
|
<option value="mtd" selected={timeRange === 'mtd'}>Month to Date</option>
|
||||||
<option value="last-month" selected={timeRange === 'last-month'}>Last Month</option>
|
<option value="ytd" selected={timeRange === 'ytd'}>Year to Date</option>
|
||||||
<option value="custom" selected={timeRange === 'custom'}>Custom Range</option>
|
<option value="last-month" selected={timeRange === 'last-month'}>Last Month</option>
|
||||||
</select>
|
<option value="custom" selected={timeRange === 'custom'}>Custom Range</option>
|
||||||
|
</select>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{timeRange === 'custom' && (
|
{timeRange === 'custom' && (
|
||||||
<>
|
<>
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">From Date</legend>
|
<legend class="fieldset-legend text-xs">From Date</legend>
|
||||||
<input
|
<AutoSubmit client:load>
|
||||||
type="date"
|
<input
|
||||||
id="reports-from"
|
type="date"
|
||||||
name="from"
|
id="reports-from"
|
||||||
class="input w-full"
|
name="from"
|
||||||
value={customFrom || (startDate.getFullYear() + '-' + String(startDate.getMonth() + 1).padStart(2, '0') + '-' + String(startDate.getDate()).padStart(2, '0'))}
|
class="input w-full"
|
||||||
onchange="this.form.submit()"
|
value={customFrom || (startDate.getFullYear() + '-' + String(startDate.getMonth() + 1).padStart(2, '0') + '-' + String(startDate.getDate()).padStart(2, '0'))}
|
||||||
/>
|
/>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">To Date</legend>
|
<legend class="fieldset-legend text-xs">To Date</legend>
|
||||||
<input
|
<AutoSubmit client:load>
|
||||||
type="date"
|
<input
|
||||||
id="reports-to"
|
type="date"
|
||||||
name="to"
|
id="reports-to"
|
||||||
class="input w-full"
|
name="to"
|
||||||
value={customTo || (endDate.getFullYear() + '-' + String(endDate.getMonth() + 1).padStart(2, '0') + '-' + String(endDate.getDate()).padStart(2, '0'))}
|
class="input w-full"
|
||||||
onchange="this.form.submit()"
|
value={customTo || (endDate.getFullYear() + '-' + String(endDate.getMonth() + 1).padStart(2, '0') + '-' + String(endDate.getDate()).padStart(2, '0'))}
|
||||||
/>
|
/>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Team Member</legend>
|
<legend class="fieldset-legend text-xs">Team Member</legend>
|
||||||
<select id="reports-member" name="member" class="select w-full" onchange="this.form.submit()">
|
<AutoSubmit client:load>
|
||||||
<option value="">All Members</option>
|
<select id="reports-member" name="member" class="select w-full">
|
||||||
{teamMembers.map(member => (
|
<option value="">All Members</option>
|
||||||
<option value={member.id} selected={selectedMemberId === member.id}>
|
{teamMembers.map(member => (
|
||||||
{member.name}
|
<option value={member.id} selected={selectedMemberId === member.id}>
|
||||||
</option>
|
{member.name}
|
||||||
))}
|
</option>
|
||||||
</select>
|
))}
|
||||||
|
</select>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Tag</legend>
|
<legend class="fieldset-legend text-xs">Tag</legend>
|
||||||
<select id="reports-tag" name="tag" class="select w-full" onchange="this.form.submit()">
|
<AutoSubmit client:load>
|
||||||
<option value="">All Tags</option>
|
<select id="reports-tag" name="tag" class="select w-full">
|
||||||
{allTags.map(tag => (
|
<option value="">All Tags</option>
|
||||||
<option value={tag.id} selected={selectedTagId === tag.id}>
|
{allTags.map(tag => (
|
||||||
{tag.name}
|
<option value={tag.id} selected={selectedTagId === tag.id}>
|
||||||
</option>
|
{tag.name}
|
||||||
))}
|
</option>
|
||||||
</select>
|
))}
|
||||||
|
</select>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Client</legend>
|
<legend class="fieldset-legend text-xs">Client</legend>
|
||||||
<select id="reports-client" name="client" class="select w-full" onchange="this.form.submit()">
|
<AutoSubmit client:load>
|
||||||
<option value="">All Clients</option>
|
<select id="reports-client" name="client" class="select w-full">
|
||||||
{allClients.map(client => (
|
<option value="">All Clients</option>
|
||||||
<option value={client.id} selected={selectedClientId === client.id}>
|
{allClients.map(client => (
|
||||||
{client.name}
|
<option value={client.id} selected={selectedClientId === client.id}>
|
||||||
</option>
|
{client.name}
|
||||||
))}
|
</option>
|
||||||
</select>
|
))}
|
||||||
|
</select>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -620,7 +632,7 @@ function getTimeRangeLabel(range: string) {
|
|||||||
<td>
|
<td>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{stat.tag.color && (
|
{stat.tag.color && (
|
||||||
<span class="w-3 h-3 rounded-full" style={`background-color: ${stat.tag.color}`}></span>
|
<ColorDot client:load color={stat.tag.color} class="w-3 h-3 rounded-full" />
|
||||||
)}
|
)}
|
||||||
<span>{stat.tag.name}</span>
|
<span>{stat.tag.name}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -736,7 +748,7 @@ function getTimeRangeLabel(range: string) {
|
|||||||
{e.tag ? (
|
{e.tag ? (
|
||||||
<div class="badge badge-xs badge-outline flex items-center gap-1">
|
<div class="badge badge-xs badge-outline flex items-center gap-1">
|
||||||
{e.tag.color && (
|
{e.tag.color && (
|
||||||
<span class="w-2 h-2 rounded-full" style={`background-color: ${e.tag.color}`}></span>
|
<ColorDot client:load color={e.tag.color} class="w-2 h-2 rounded-full" />
|
||||||
)}
|
)}
|
||||||
<span>{e.tag.name}</span>
|
<span>{e.tag.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
|
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
|
||||||
import Icon from '../../../components/Icon.astro';
|
import Icon from '../../../components/Icon.astro';
|
||||||
|
import ModalButton from '../../../components/ModalButton.vue';
|
||||||
|
import ConfirmForm from '../../../components/ConfirmForm.vue';
|
||||||
|
import ColorDot from '../../../components/ColorDot.vue';
|
||||||
import { db } from '../../../db';
|
import { db } from '../../../db';
|
||||||
import { organizations, tags } from '../../../db/schema';
|
import { organizations, tags } from '../../../db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
@@ -234,10 +237,10 @@ const successType = url.searchParams.get('success');
|
|||||||
<Icon name="tag" class="w-4 h-4" />
|
<Icon name="tag" class="w-4 h-4" />
|
||||||
Tags & Rates
|
Tags & Rates
|
||||||
</h2>
|
</h2>
|
||||||
<button onclick="document.getElementById('new_tag_modal').showModal()" class="btn btn-primary btn-xs">
|
<ModalButton client:load modalId="new_tag_modal" class="btn btn-primary btn-xs">
|
||||||
<Icon name="plus" class="w-3 h-3" />
|
<Icon name="plus" class="w-3 h-3" />
|
||||||
Add Tag
|
Add Tag
|
||||||
</button>
|
</ModalButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-base-content/60 text-xs mb-4">
|
<p class="text-base-content/60 text-xs mb-4">
|
||||||
@@ -268,7 +271,7 @@ const successType = url.searchParams.get('success');
|
|||||||
<td>
|
<td>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{tag.color && (
|
{tag.color && (
|
||||||
<div class="w-3 h-3 rounded-full" style={`background-color: ${tag.color}`}></div>
|
<ColorDot client:load color={tag.color} class="w-3 h-3 rounded-full" />
|
||||||
)}
|
)}
|
||||||
<span class="font-medium">{tag.name}</span>
|
<span class="font-medium">{tag.name}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,17 +285,18 @@ const successType = url.searchParams.get('success');
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<button
|
<ModalButton
|
||||||
onclick={`document.getElementById('edit_tag_modal_${tag.id}').showModal()`}
|
client:load
|
||||||
|
modalId={`edit_tag_modal_${tag.id}`}
|
||||||
class="btn btn-ghost btn-xs btn-square"
|
class="btn btn-ghost btn-xs btn-square"
|
||||||
>
|
>
|
||||||
<Icon name="pencil" class="w-3 h-3" />
|
<Icon name="pencil" class="w-3 h-3" />
|
||||||
</button>
|
</ModalButton>
|
||||||
<form method="POST" action={`/api/tags/${tag.id}/delete`} onsubmit="return confirm('Are you sure you want to delete this tag?');">
|
<ConfirmForm client:load message="Are you sure you want to delete this tag?" action={`/api/tags/${tag.id}/delete`}>
|
||||||
<button class="btn btn-ghost btn-xs btn-square text-error">
|
<button class="btn btn-ghost btn-xs btn-square text-error">
|
||||||
<Icon name="trash" class="w-3 h-3" />
|
<Icon name="trash" class="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</ConfirmForm>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Edit Modal */}
|
{/* Edit Modal */}
|
||||||
@@ -314,7 +318,7 @@ const successType = url.searchParams.get('success');
|
|||||||
<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/40 mt-1">Enter rate in cents (e.g. 5000 = $50.00)</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<button type="button" class="btn btn-sm" onclick={`document.getElementById('edit_tag_modal_${tag.id}').close()`}>Cancel</button>
|
<ModalButton client:load modalId={`edit_tag_modal_${tag.id}`} action="close" class="btn btn-sm">Cancel</ModalButton>
|
||||||
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -352,7 +356,7 @@ const successType = url.searchParams.get('success');
|
|||||||
<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/40 mt-1">Enter rate in cents (e.g. 5000 = $50.00)</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<button type="button" class="btn btn-sm" onclick="document.getElementById('new_tag_modal').close()">Cancel</button>
|
<ModalButton client:load modalId="new_tag_modal" action="close" class="btn btn-sm">Cancel</ModalButton>
|
||||||
<button type="submit" class="btn btn-primary btn-sm">Create Tag</button>
|
<button type="submit" class="btn btn-primary btn-sm">Create Tag</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -363,5 +367,4 @@ const successType = url.searchParams.get('success');
|
|||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import DashboardLayout from '../../layouts/DashboardLayout.astro';
|
|||||||
import Icon from '../../components/Icon.astro';
|
import Icon from '../../components/Icon.astro';
|
||||||
import Timer from '../../components/Timer.vue';
|
import Timer from '../../components/Timer.vue';
|
||||||
import ManualEntry from '../../components/ManualEntry.vue';
|
import ManualEntry from '../../components/ManualEntry.vue';
|
||||||
|
import AutoSubmit from '../../components/AutoSubmit.vue';
|
||||||
|
import ConfirmForm from '../../components/ConfirmForm.vue';
|
||||||
import { db } from '../../db';
|
import { db } from '../../db';
|
||||||
import { timeEntries, clients, tags, users } from '../../db/schema';
|
import { timeEntries, clients, tags, users } from '../../db/schema';
|
||||||
import { eq, desc, asc, and, sql, or, like } from 'drizzle-orm';
|
import { eq, desc, asc, and, sql, or, like } from 'drizzle-orm';
|
||||||
@@ -206,42 +208,50 @@ const paginationPages = getPaginationPages(page, totalPages);
|
|||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Client</legend>
|
<legend class="fieldset-legend text-xs">Client</legend>
|
||||||
<select id="tracker-client" name="client" class="select w-full" onchange="this.form.submit()">
|
<AutoSubmit client:load>
|
||||||
<option value="">All Clients</option>
|
<select id="tracker-client" name="client" class="select w-full">
|
||||||
{allClients.map(client => (
|
<option value="">All Clients</option>
|
||||||
<option value={client.id} selected={filterClient === client.id}>
|
{allClients.map(client => (
|
||||||
{client.name}
|
<option value={client.id} selected={filterClient === client.id}>
|
||||||
</option>
|
{client.name}
|
||||||
))}
|
</option>
|
||||||
</select>
|
))}
|
||||||
|
</select>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Status</legend>
|
<legend class="fieldset-legend text-xs">Status</legend>
|
||||||
<select id="tracker-status" name="status" class="select w-full" onchange="this.form.submit()">
|
<AutoSubmit client:load>
|
||||||
<option value="" selected={filterStatus === ''}>All Entries</option>
|
<select id="tracker-status" name="status" class="select w-full">
|
||||||
<option value="completed" selected={filterStatus === 'completed'}>Completed</option>
|
<option value="" selected={filterStatus === ''}>All Entries</option>
|
||||||
<option value="running" selected={filterStatus === 'running'}>Running</option>
|
<option value="completed" selected={filterStatus === 'completed'}>Completed</option>
|
||||||
</select>
|
<option value="running" selected={filterStatus === 'running'}>Running</option>
|
||||||
|
</select>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Entry Type</legend>
|
<legend class="fieldset-legend text-xs">Entry Type</legend>
|
||||||
<select id="tracker-type" name="type" class="select w-full" onchange="this.form.submit()">
|
<AutoSubmit client:load>
|
||||||
<option value="" selected={filterType === ''}>All Types</option>
|
<select id="tracker-type" name="type" class="select w-full">
|
||||||
<option value="timed" selected={filterType === 'timed'}>Timed</option>
|
<option value="" selected={filterType === ''}>All Types</option>
|
||||||
<option value="manual" selected={filterType === 'manual'}>Manual</option>
|
<option value="timed" selected={filterType === 'timed'}>Timed</option>
|
||||||
</select>
|
<option value="manual" selected={filterType === 'manual'}>Manual</option>
|
||||||
|
</select>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend text-xs">Sort By</legend>
|
<legend class="fieldset-legend text-xs">Sort By</legend>
|
||||||
<select id="tracker-sort" name="sort" class="select w-full" onchange="this.form.submit()">
|
<AutoSubmit client:load>
|
||||||
<option value="start-desc" selected={sortBy === 'start-desc'}>Newest First</option>
|
<select id="tracker-sort" name="sort" class="select w-full">
|
||||||
<option value="start-asc" selected={sortBy === 'start-asc'}>Oldest First</option>
|
<option value="start-desc" selected={sortBy === 'start-desc'}>Newest First</option>
|
||||||
<option value="duration-desc" selected={sortBy === 'duration-desc'}>Longest Duration</option>
|
<option value="start-asc" selected={sortBy === 'start-asc'}>Oldest First</option>
|
||||||
<option value="duration-asc" selected={sortBy === 'duration-asc'}>Shortest Duration</option>
|
<option value="duration-desc" selected={sortBy === 'duration-desc'}>Longest Duration</option>
|
||||||
</select>
|
<option value="duration-asc" selected={sortBy === 'duration-asc'}>Shortest Duration</option>
|
||||||
|
</select>
|
||||||
|
</AutoSubmit>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<input type="hidden" name="page" value="1" />
|
<input type="hidden" name="page" value="1" />
|
||||||
@@ -322,15 +332,14 @@ const paginationPages = getPaginationPages(page, totalPages);
|
|||||||
</td>
|
</td>
|
||||||
<td class="font-mono font-semibold text-primary text-sm">{formatTimeRange(entry.startTime, entry.endTime)}</td>
|
<td class="font-mono font-semibold text-primary text-sm">{formatTimeRange(entry.startTime, entry.endTime)}</td>
|
||||||
<td>
|
<td>
|
||||||
<form method="POST" action={`/api/time-entries/${entry.id}/delete`} class="inline">
|
<ConfirmForm client:load message="Are you sure you want to delete this entry?" action={`/api/time-entries/${entry.id}/delete`} class="inline">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-ghost btn-xs text-error"
|
class="btn btn-ghost btn-xs text-error"
|
||||||
onclick="return confirm('Are you sure you want to delete this entry?')"
|
|
||||||
>
|
>
|
||||||
<Icon name="trash" class="w-3.5 h-3.5" />
|
<Icon name="trash" class="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</ConfirmForm>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user