-
+
diff --git a/resources/js/components/ui/table/TableHead.vue b/resources/js/components/ui/table/TableHead.vue
index 06d0022..ddf635c 100644
--- a/resources/js/components/ui/table/TableHead.vue
+++ b/resources/js/components/ui/table/TableHead.vue
@@ -8,10 +8,7 @@ const props = defineProps<{
-
+
diff --git a/resources/js/components/ui/table/TableHeader.vue b/resources/js/components/ui/table/TableHeader.vue
index b4ab5cf..66e1e60 100644
--- a/resources/js/components/ui/table/TableHeader.vue
+++ b/resources/js/components/ui/table/TableHeader.vue
@@ -8,10 +8,7 @@ const props = defineProps<{
-
+
diff --git a/resources/js/components/ui/table/TableRow.vue b/resources/js/components/ui/table/TableRow.vue
index 7c29a3a..036db30 100644
--- a/resources/js/components/ui/table/TableRow.vue
+++ b/resources/js/components/ui/table/TableRow.vue
@@ -8,10 +8,7 @@ const props = defineProps<{
-
+
diff --git a/resources/js/layouts/AppLayout.vue b/resources/js/layouts/AppLayout.vue
index f11f2d4..770fc26 100644
--- a/resources/js/layouts/AppLayout.vue
+++ b/resources/js/layouts/AppLayout.vue
@@ -6,7 +6,7 @@ import 'vue-sonner/style.css'
import { Toaster } from 'vue-sonner'
import { Info, CircleAlert, CircleCheck, LoaderCircle, Ban } from "lucide-vue-next"
import { Button } from '@/components/ui/crm-button'
-import { SidebarProvider } from '@/components/ui/sidebar';
+import { SidebarProvider } from '@/components/ui/crm-sidebar';
import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'
import { alertStore } from '@/stores/alertStore';
import { webcronStore } from '@/stores/webcronStore';
@@ -70,12 +70,12 @@ onMounted(() => {
-
+
-
+
@@ -115,12 +115,5 @@ onMounted(() => {
background-color: transparent;
}
- main {
- margin: 0;
- background-color: transparent;
- border-radius: 0;
- box-shadow: none;
- outline: none;
- }
}
\ No newline at end of file
diff --git a/resources/js/layouts/auth/AuthSplitLayout.vue b/resources/js/layouts/auth/AuthSplitLayout.vue
index e24e5a1..a4246a7 100644
--- a/resources/js/layouts/auth/AuthSplitLayout.vue
+++ b/resources/js/layouts/auth/AuthSplitLayout.vue
@@ -1,6 +1,5 @@
-
-
-
-
-
-
-
-
diff --git a/resources/js/pages/Customers.vue b/resources/js/pages/Customers.vue
index 946aa65..083b0a5 100644
--- a/resources/js/pages/Customers.vue
+++ b/resources/js/pages/Customers.vue
@@ -5,7 +5,7 @@ import AppLayout from '@/layouts/AppLayout.vue'
import AppHeader from '@/components/AppHeader.vue'
import { Address, Customer } from '@/types'
import { newCustomer } from '@/types/index.d'
-import { bgColorForString } from '@/lib/utils'
+import { hotkey, bgColorForString } from '@/lib/utils'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Input } from '@/components/ui/crm-input'
import { Button } from '@/components/ui/crm-button'
@@ -40,6 +40,9 @@ onMounted(() => {
customersData.value = props.customersData.toSorted((a, b) => (a.companyName ?? '').localeCompare(b.companyName ?? ''));
searchField.value = document.getElementById('search')
searchField.value.focus()
+
+ // Register hotkeys
+ hotkey('mod+f', () => { searchField.value.focus() })
})
watch(activeCustomer, () => {
@@ -156,10 +159,10 @@ const call = (number: string, event: Event) => {
@@ -170,11 +173,11 @@ const call = (number: string, event: Event) => {
-
+
-
+
@@ -184,7 +187,7 @@ const call = (number: string, event: Event) => {
-
+
Neu
@@ -207,7 +210,7 @@ const call = (number: string, event: Event) => {
@@ -232,7 +235,7 @@ const call = (number: string, event: Event) => {
-
+
{{ customer.url }}
@@ -241,7 +244,7 @@ const call = (number: string, event: Event) => {
-
+
{{ customer.phone }}
@@ -250,7 +253,7 @@ const call = (number: string, event: Event) => {
-
+
@@ -276,9 +279,9 @@ const call = (number: string, event: Event) => {
-
+
- {{ contact.academicTitle }}
+ {{ contact.academicTitle }}
{{ contact.firstName + ' ' + contact.lastName }}
{{ contact.jobTitle }}
@@ -287,17 +290,17 @@ const call = (number: string, event: Event) => {
-
+
-
+
-
+
{
-
-
+
+
+
{{ phoneNumber }}
Anrufen
-
- {{ phoneNumber }}
-
Schließen
-
+
Anrufen
diff --git a/resources/js/pages/Dashboard.vue b/resources/js/pages/Dashboard.vue
index 7d8d80f..4fdbe7e 100644
--- a/resources/js/pages/Dashboard.vue
+++ b/resources/js/pages/Dashboard.vue
@@ -1,19 +1,22 @@
+
+
+
-
-
-
-
-
-
+
- Aufgaben
+ Benachrichtigungen
Card Description
+
+
+
+
+ Du hast zwei neue Erfolge
+
+ Weiter so
+
+
+
+
+
+
+
+
+
+ Du hast länger keine Bestandskunden mehr kontaktiert
+
+ Hier sind ein paar Vorschläge für Dich
+
+
+
+
+
+
+
+
+
+
+
+ Alles leeren
+
+
+
+
+
+
+
+
+ Aufgaben
+ Card Description
+
+
+
+
+ Erledigte
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
!!!
-
!!
-
!
-
-
{{
- todo.title
- }}
-
-
-
- {{ toLocalDate(todo.dueDate) }}
-
-
-
-
-
-
-
-
-
-
+
@@ -168,14 +152,11 @@ const todos = ref([])
- {{
+ {{
toRoundedCurrency(salesStatistics?.paid) || '' }}
{{ toRoundedCurrency(salesTarget) }}
-
-
diff --git a/resources/js/pages/Invoices.vue b/resources/js/pages/Invoices.vue
index 8f4b10e..38618b0 100644
--- a/resources/js/pages/Invoices.vue
+++ b/resources/js/pages/Invoices.vue
@@ -5,6 +5,7 @@ import { type Invoice } from '@/types'
import { newInvoice } from '@/types/index.d'
import axios from 'axios'
import AppLayout from '@/layouts/AppLayout.vue'
+import AppHeader from '@/components/AppHeader.vue'
import { Button } from '@/components/ui/crm-button'
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select'
import DocumentTable from '@/components/documents/DocumentTable.vue'
@@ -15,11 +16,10 @@ import SelectSeparator from '@/components/ui/select/SelectSeparator.vue'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { statusBadgeLabels } from '@/components/ui/status-badge'
-import AppHeader from '@/components/AppHeader.vue'
import InvoiceDialog from '@/components/documents/InvoiceDialog.vue'
import { hotkey, getPlatformModifierSymbol } from '@/lib/utils'
-// initial invoice data from inertia
+// Initial invoice data from inertia (see InvoiceController::show)
interface Props {
invoicesData: Invoice[];
}
@@ -49,7 +49,7 @@ onMounted(async () => {
searchField.value = document.getElementById('search')
- // register hotkeys
+ // Register hotkeys
hotkey('n', createInvoice)
hotkey('mod+leftarrow', () => {
if (selectedYearIndex.value < (years.value.length - 1)) {
@@ -87,6 +87,7 @@ const fuse = computed(() => {
'title',
'nr'
],
+ useExtendedSearch: true,
threshold: 0.3,
}
@@ -211,11 +212,11 @@ const onDeleteInvoice = async (id: number) => {
-
+
-
+
@@ -225,7 +226,7 @@ const onDeleteInvoice = async (id: number) => {
-
+
Neu
diff --git a/resources/js/pages/Pipeline.vue b/resources/js/pages/Pipeline.vue
new file mode 100644
index 0000000..db98e8e
--- /dev/null
+++ b/resources/js/pages/Pipeline.vue
@@ -0,0 +1,354 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Eine Vetriebspipeline ist ein visuelles und strukturelles System, das den Fortschritt
+ potenzieller Kunden – von der ersten Kontaktaufnahme (Akquise) bis zum
+ abgeschlossenen
+ Verkauf (Projekt) – abbildet. Sie hilft Teams, Chancen zu priorisieren, nächste
+ Schritte
+ zu planen und den Vertriebsprozess transparent zu gestalten.
+
+ Mehr zum Thema Vertrieb in Caramel
+
+
+
+
+
+ Neue Karte
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ lane.title }}
+ {{ toCurrency(laneSums[lane.id] || 0) }}
+
+
+ {{ lane.items?.length }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ item.title }}
+
{{
+ toCurrency(item.expectedRevenue) }}
+
+
+
+ {{ item.actions }}
+
+
+
+
+ Heute
+ {{ toDuration(item.dueDate) }}
+
+
+
+ {{ item.notes.length }}
+
+
+ {{ item.notesCount }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Löschen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/js/pages/ProceduralDocumentation.vue b/resources/js/pages/ProceduralDocumentation.vue
index c150511..0c38a1d 100644
--- a/resources/js/pages/ProceduralDocumentation.vue
+++ b/resources/js/pages/ProceduralDocumentation.vue
@@ -1,6 +1,6 @@
diff --git a/resources/js/pages/Products.vue b/resources/js/pages/Products.vue
index b023e2d..499de7e 100644
--- a/resources/js/pages/Products.vue
+++ b/resources/js/pages/Products.vue
@@ -124,11 +124,11 @@ const toggleAllCategories = () => {
-
+
-
+
@@ -137,7 +137,7 @@ const toggleAllCategories = () => {
-
+
Neu
diff --git a/resources/js/pages/Timesheets.vue b/resources/js/pages/Timesheets.vue
index ae269d8..a6ecddd 100644
--- a/resources/js/pages/Timesheets.vue
+++ b/resources/js/pages/Timesheets.vue
@@ -1,6 +1,199 @@
@@ -8,5 +201,149 @@ import { Head } from '@inertiajs/vue3';
+
+
+
+
{ timesheets[selectedTimesheet].title = value; updateTimesheet(); }"
+ default-value="Zeiterfassung" placeholder="Zeiterfassung"
+ class="text-lg! text-primary font-semibold" />
+ Zeiterfassung
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Rechnung stellen
+
+
+
+
+
+
+
+ Löschen
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/pages/auth/TwoFactorChallenge.vue b/resources/js/pages/auth/TwoFactorChallenge.vue
index 70687f9..3b07544 100644
--- a/resources/js/pages/auth/TwoFactorChallenge.vue
+++ b/resources/js/pages/auth/TwoFactorChallenge.vue
@@ -17,16 +17,16 @@ interface AuthConfigContent {
const authConfigContent = computed(() => {
if (showRecoveryInput.value) {
return {
- title: 'Recovery Code',
- description: 'Please confirm access to your account by entering one of your emergency recovery codes.',
- toggleText: 'login using an authentication code',
+ title: 'Wiederherstellungscode',
+ description: 'Bitte bestätige den Zugriff auf Dein Konto, indem Du einen Deiner Notfall-Wiederherstellungscodes eingeben.',
+ toggleText: 'mit Authentifizierungscode anmelden',
};
}
return {
title: 'Authentication Code',
- description: 'Enter the authentication code provided by your authenticator application.',
- toggleText: 'login using a recovery code',
+ description: 'Bitte gib das Einmalkennwort aus Deiner Authentifizierungs-App ein.',
+ toggleText: 'mit einem Wiederherstellungscode anmelden',
};
});
@@ -44,51 +44,46 @@ const codeValue = computed(() => code.value.join(''));
+
-
-
diff --git a/resources/js/pages/settings/TwoFactor.vue b/resources/js/pages/settings/TwoFactor.vue
index 3eea054..d25414f 100644
--- a/resources/js/pages/settings/TwoFactor.vue
+++ b/resources/js/pages/settings/TwoFactor.vue
@@ -2,7 +2,7 @@
import HeadingSmall from '@/components/HeadingSmall.vue';
import TwoFactorRecoveryCodes from '@/components/TwoFactorRecoveryCodes.vue';
import TwoFactorSetupModal from '@/components/TwoFactorSetupModal.vue';
-import { Badge } from '@/components/ui/badge';
+import { Badge } from '@/components/ui/crm-badge';
import { Button } from '@/components/ui/crm-button';
import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
import AppLayout from '@/layouts/AppLayout.vue';
diff --git a/resources/js/services/NotesService.ts b/resources/js/services/NotesService.ts
new file mode 100644
index 0000000..389bf8e
--- /dev/null
+++ b/resources/js/services/NotesService.ts
@@ -0,0 +1,59 @@
+import axios, { AxiosError } from 'axios';
+import { Note, PipelineItem } from '@/types';
+import { toast } from 'vue-sonner';
+
+const API_URL = '/api/notes';
+
+export default {
+ /**
+ * Retrieves all notes
+ * @returns Promise
[] | null>
+ */
+ async getAllNotes(modelType: string, notableId: number): Promise[] | null> {
+ try {
+ const response = await axios.get[]>(`${API_URL}/${modelType}/${notableId}`)
+ return response.data;
+ } catch (error) {
+ toast.error('Fehler beim Laden der Notizen', { description: (error as AxiosError).message })
+ console.error(error)
+ }
+
+ return null
+ },
+
+ /**
+ * Creates a new note
+ * @param note - The note data to create
+ * @returns Promise
+ */
+ async createNote(note: Partial): Promise {
+ const response = await axios.post(API_URL, note);
+ return response.data;
+ },
+
+ /**
+ * Update an existing note
+ * @param note - The Note object to update
+ * @returns
+ */
+ async updateNote(note: Partial): Promise {
+ const response = await axios.put(`${API_URL}/${note.id}`, note);
+ return response.data;
+ },
+
+ /**
+ * Deletes a note by ID
+ * @param noteId - The id of the note to delete
+ * @returns boolean - True if the note was deleted, false otherwise
+ */
+ async deleteNote(noteId: number): Promise {
+ try {
+ const response = await axios.delete(`${API_URL}/${noteId}`);
+ return true;
+ } catch (error) {
+ toast.error('Fehler beim LÖschen der Notiz', { description: (error as AxiosError).message })
+ console.error(error)
+ return false;
+ }
+ },
+};
\ No newline at end of file
diff --git a/resources/js/services/PipelineService.ts b/resources/js/services/PipelineService.ts
new file mode 100644
index 0000000..db5fbff
--- /dev/null
+++ b/resources/js/services/PipelineService.ts
@@ -0,0 +1,24 @@
+import axios, { AxiosError } from 'axios';
+import { Note, PipelineItem } from '@/types';
+import { toast } from 'vue-sonner';
+
+const API_URL = '/api/pipeline';
+const ITEMS_API_URL = '/api/pipelineItems';
+
+export default {
+ /**
+ * Deletes an item by ID
+ * @param id - The id of the item to delete
+ * @returns boolean - True if the item was deleted, false otherwise
+ */
+ async deletePipelineItem(id: number): Promise {
+ try {
+ const response = await axios.delete(`${ITEMS_API_URL}/${id}`);
+ return true;
+ } catch (error) {
+ toast.error('Fehler beim Löschen des Vorgangs', { description: (error as AxiosError).message })
+ console.error(error)
+ return false;
+ }
+ },
+};
\ No newline at end of file
diff --git a/resources/js/services/TimesheetService.ts b/resources/js/services/TimesheetService.ts
new file mode 100644
index 0000000..51e20c9
--- /dev/null
+++ b/resources/js/services/TimesheetService.ts
@@ -0,0 +1,106 @@
+// resources/js/services/TimesheetService.ts
+import axios, { AxiosResponse } from 'axios';
+import { Timesheet, TimesheetEntry } from '@/types';
+import { update } from '@/routes/password';
+
+const API_URL = '/api/timesheets';
+const ENTRY_API_URL = '/api/timesheet-entries';
+
+export default {
+ /**
+ * Retrieves all timesheets
+ * @returns Promise
+ */
+ async getAllTimesheets(): Promise {
+ const response = await axios.get(API_URL);
+ return response.data;
+ },
+
+ /**
+ * Creates a new timesheet
+ * @param timesheet - The timesheet data to create
+ * @returns Promise
+ */
+ async createTimesheet(timesheet: Partial): Promise {
+ const response = await axios.post(API_URL, timesheet);
+ return response.data;
+ },
+
+ /**
+ * Update an existing timesheet
+ * @param timesheet - The Timesheet object to update
+ * @returns
+ */
+ async updateTimesheet(timesheet: Partial): Promise {
+ const response = await axios.put(`${API_URL}/${timesheet.id}`, timesheet);
+ return response.data;
+ },
+
+ /**
+ * Deletes a timesheet by ID
+ * @param timesheetId - The id of the timesheet to delete
+ * @returns Promise
+ */
+ async deleteTimesheet(timesheetId: number): Promise {
+ const response = await axios.delete(`${API_URL}/${timesheetId}`);
+ return response.data;
+ },
+
+ /**
+ * Retrieves all entries for a specific timesheet
+ * @param timesheetId - The ID of the timesheet
+ * @returns Promise
+ */
+ async getTimesheetEntries(timesheetId: number): Promise {
+ const response = await axios.get(`${API_URL}/${timesheetId}/entries`);
+ return response.data;
+ },
+
+ /**
+ * Retrieves all timesheet entries
+ * @returns Promise
+ */
+ async getAllEntries(): Promise {
+ const response = await axios.get(ENTRY_API_URL);
+ return response.data;
+ },
+
+ /**
+ * Creates a new timesheet entry
+ * @param entry - The entry data to create
+ * @returns Promise
+ */
+ async createEntry(entry: Partial): Promise {
+ const response = await axios.post(ENTRY_API_URL, entry);
+ return response.data;
+ },
+
+ /**
+ * Updates an existing timesheet entry
+ * @param entry - The TimesheetEntry object to update
+ * @returns Promise
+ */
+ async updateEntry(entry: Partial): Promise {
+ const response = await axios.put(`${ENTRY_API_URL}/${entry.id}`, entry);
+ return response.data;
+ },
+
+ /**
+ * Toggles the billed status of a timesheet entry
+ * @param entryId - The ID of the entry to toggle
+ * @returns Promise
+ */
+ async toggleBilled(entryId: number): Promise {
+ const response = await axios.patch(`${ENTRY_API_URL}/${entryId}/toggle-billed`);
+ return response.data;
+ },
+
+ /**
+ * Deletes a timesheet entry
+ * @param entryId - The ID of the entry to delete
+ * @returns Promise
+ */
+ async deleteEntry(entryId: number): Promise {
+ await axios.delete(`${ENTRY_API_URL}/${entryId}`);
+ }
+};
\ No newline at end of file
diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts
index aaacfba..505b470 100644
--- a/resources/js/types/index.d.ts
+++ b/resources/js/types/index.d.ts
@@ -39,8 +39,8 @@ export interface User {
email: string;
avatar?: string;
email_verified_at: string | null;
- created_at: string;
- updated_at: string;
+ createdAt: string;
+ updatedAt: string;
}
@@ -104,30 +104,28 @@ export function newContact(): Contact {
}
}
-export interface Notable {
- id: number;
- notes?: Note[];
-}
-
-export interface Note {
+export interface Note {
id: number;
user: User;
text: string;
- notable_id: number;
- notable_type: string;
- notable?: T;
- createdAt: Date;
+ notableId: number;
+ notableType: NoteableType;
+ createdAt: string;
+ updatedAt: string;
}
-export function newNote(user: User, notable: T): Note {
+export type NoteableType = 'PipelineItem' | 'Customer'
+
+export function newNote(user: User, text: string, notableId: number, noteableType: NoteableType): Note {
return {
id: 0,
user: user,
- text: '',
- notable_id: notable.id,
- notable_type: notable.constructor.name,
- notable: notable
- };
+ text: text,
+ notableId: notableId,
+ notableType: noteableType,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString()
+ }
}
export interface Customer {
@@ -185,12 +183,12 @@ export interface Todo {
typeId: number | null;
type?: TodoType | null;
url: string | null;
- dueDate: Date | null;
+ dueDate: string | null;
recurring: boolean;
priority: number;
status: string;
- createdAt: Date;
- lastModified: Date;
+ createdAt: string;
+ lastModified: string;
parent: string | null;
parentTodo?: Todo | null;
children?: Todo[];
@@ -210,8 +208,8 @@ export function newTodo(): Todo {
recurring: false,
priority: 1,
status: 'pending',
- createdAt: new Date(),
- lastModified: new Date(),
+ createdAt: new Date().toISOString(),
+ lastModified: new Date().toISOString(),
parent: null,
parentTodo: null,
children: [],
@@ -251,7 +249,7 @@ export interface Invoice {
export type InvoiceType = Invoice
export function newInvoice(): Invoice {
- const date = new Date();
+ const date = new Date().toISOString();
return {
id: 0,
@@ -340,16 +338,16 @@ export function newPaymentTerms(): PaymentTerms {
export interface ProductCategory {
id: number;
name: string;
- createdAt: Date;
- updatedAt: Date;
+ createdAt: string;
+ updatedAt: string;
}
export function newProductCategory(): ProductCategory {
return {
id: 0,
name: '',
- createdAt: new Date(),
- updatedAt: new Date()
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString()
}
}
@@ -357,8 +355,8 @@ export interface Unit {
id: number;
name: string;
symbol: string | null;
- createdAt: Date;
- updatedAt: Date;
+ createdAt: string;
+ updatedAt: string;
}
export function newUnit(): Unit {
@@ -366,8 +364,8 @@ export function newUnit(): Unit {
id: 2,
name: 'Stunden',
symbol: 'h',
- createdAt: new Date(),
- updatedAt: new Date()
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString()
}
}
@@ -384,8 +382,8 @@ export interface Product {
unitId: number | null;
unit: Unit | null;
image: string | null;
- createdAt: Date;
- updatedAt: Date;
+ createdAt: string;
+ updatedAt: string;
}
export function newProduct(): Product {
@@ -402,7 +400,117 @@ export function newProduct(): Product {
unitId: null,
unit: null,
image: null,
- createdAt: new Date(),
- updatedAt: new Date()
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString()
}
+}
+
+
+// Pipeline item used by CRM pipeline
+export interface PipelineItem {
+ id: number;
+ pipelineLaneId: number;
+ title: string;
+ position: number;
+ expectedRevenue: number | null;
+ dueDate: string | null;
+ description: string | null;
+ notes?: Note[];
+ createdAt?: string;
+ updatedAt?: string;
+}
+
+export function newPipelineItem(): PipelineItem {
+ return {
+ id: 0,
+ title: 'Neue Spalte',
+ position: 0,
+ expectedRevenue: null,
+ dueDate: null,
+ description: null,
+ notes: [],
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ }
+}
+
+// Pipeline lane used by CRM pipeline
+export interface PipelineLane {
+ id: number;
+ title: string;
+ position: number;
+ createdAt?: string;
+ updatedAt?: string;
+ items?: PipelineItem[];
+}
+
+export function newPipelineLane(): PipelineLane {
+ return {
+ id: 0,
+ title: '',
+ position: 0,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ items: [],
+ };
+}
+
+// Timesheet Types
+export interface Timesheet {
+ id: number;
+ title: string;
+ createdAt: string;
+ updatedAt: string;
+ entries?: TimesheetEntry[];
+ totalHours?: number;
+ hoursBilled?: number;
+ earliestDate?: string | null;
+ latestDate?: string | null;
+}
+
+export type TimesheetType = Timesheet;
+
+export function newTimesheet(): Timesheet {
+ return {
+ id: 0,
+ title: 'Neuer Stundenzettel',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ entries: [],
+ totalHours: 0,
+ hoursBilled: 0,
+ earliestDate: null,
+ latestDate: null
+ };
+}
+
+// TimesheetEntry Types
+export interface TimesheetEntry {
+ id: number;
+ timesheetId: number;
+ date: string; // ISO format
+ userId: number;
+ description: string | null;
+ hours: number;
+ billed: boolean;
+ createdAt: string;
+ updatedAt: string;
+ timesheet?: Timesheet;
+ user?: User;
+}
+
+export type TimesheetEntryType = TimesheetEntry;
+
+export function newTimesheetEntry(): TimesheetEntry {
+ return {
+ id: 0,
+ timesheetId: 0,
+ date: new Date().toISOString(),
+ userId: 0,
+ description: null,
+ hours: 0,
+ billed: false,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString()
+ };
}
\ No newline at end of file
diff --git a/routes/api.php b/routes/api.php
index f39b68f..d5e9a04 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -12,21 +12,29 @@
use App\Http\Controllers\UnitController;
use App\Mail\OrderConfirmation;
use App\Services\CaldavService;
+use App\Http\Controllers\TimesheetController;
+use App\Http\Controllers\TimesheetEntryController;
+use App\Http\Controllers\PipelineController;
+use App\Http\Controllers\PipelineItemController;
+
+Route::get('/pipeline', [PipelineController::class, 'index']);
+Route::post('/pipeline/positions', [PipelineController::class, 'updatePositions']);
+
+Route::get('/pipelineItems', [PipelineItemController::class, 'index']);
+Route::get('/pipelineItems/{id}', [PipelineItemController::class, 'single']);
+Route::post('/pipelineItems', [PipelineItemController::class, 'store']);
+Route::put('/pipelineItems/{id}', [PipelineItemController::class, 'update']);
+Route::delete('/pipelineItems/{id}', [PipelineItemController::class, 'delete']);
Route::get('/customers/{id}', [CustomerController::class, 'single']);
Route::get('/customers', [CustomerController::class, 'index']);
-Route::get('/customers/{id}/notes', [NoteController::class, 'index'])
- ->defaults('modelType', 'customer')
- ->name('customers.notes.index');
-
-Route::post('/customers/{id}/notes', [NoteController::class, 'store'])
- ->defaults('modelType', 'customer')
+Route::get('/notes/{modelType}/{notableId}', [NoteController::class, 'index']);
+Route::delete('/notes/{id}', [NoteController::class, 'delete']);
+Route::post('/notes', [NoteController::class, 'store'])
->name('customers.notes.store');
-Route::delete('/notes/{id}', [NoteController::class, 'delete']);
-
Route::get('/todo-types', function () {
return \App\Models\TodoType::all();
});
@@ -99,3 +107,8 @@
Route::post('/settings', [SettingController::class, 'update']);
Route::apiResource('/units', UnitController::class);
+
+Route::apiResource('timesheets', TimesheetController::class);
+Route::get('timesheets/{timesheet}/entries', [TimesheetEntryController::class, 'getEntriesForTimesheet']);
+Route::apiResource('timesheet-entries', TimesheetEntryController::class);
+Route::patch('timesheet-entries/{timesheetEntry}/toggle-billed', [TimesheetEntryController::class, 'toggleBilled']);
diff --git a/routes/web.php b/routes/web.php
index c862a1f..8272cca 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -9,23 +9,20 @@
use App\Http\Controllers\InvoiceController;
use App\Http\Controllers\CustomerController;
use App\Http\Controllers\ProductController;
-use App\Models\Setting;
+use App\Http\Controllers\TimesheetController;
+use App\Http\Controllers\PipelineController;
Route::middleware('auth')->group(function () {
// Dashboard
- Route::get('/', function () {
- return Inertia::render('Dashboard');
- })->name('home');
+ Route::redirect('/', '/dashboard');
Route::get('dashboard', function () {
return Inertia::render('Dashboard');
})->name('dashboard');
// CRM
- Route::get('crm', function () {
- return Inertia::render('CRM');
- })->name('crm');
+ Route::get('pipeline', [PipelineController::class, 'show'])->name('pipeline');
// Offers
Route::get('offers', function () {
@@ -55,9 +52,8 @@
// Products
Route::get('products', [ProductController::class, 'show'])->name('products');
- Route::get('timesheets', function () {
- return Inertia::render('Timesheets');
- })->name('timesheets');
+ // Timesheets
+ Route::get('timesheets', [TimesheetController::class, 'show'])->name('timesheets');
// Procedural Documentation
Route::get('proceduralDocumentation', function () {