diff --git a/app/Console/Commands/CaldavSyncCommand.php b/app/Console/Commands/CaldavSyncCommand.php new file mode 100644 index 0000000..2e8d6e1 --- /dev/null +++ b/app/Console/Commands/CaldavSyncCommand.php @@ -0,0 +1,32 @@ +info('Starting CalDAV sync...'); + + $todos = $service->getTodos(); + + $count = 0; + foreach ($todos as $todo) { + Todo::upsert($todo->attributesToArray(), 'id'); + $count++; + } + + $this->info("Synced " . count($todos) . " todos."); + return 0; + } +} diff --git a/app/Console/Commands/Kernel.php b/app/Console/Commands/Kernel.php new file mode 100644 index 0000000..ac665f8 --- /dev/null +++ b/app/Console/Commands/Kernel.php @@ -0,0 +1,41 @@ +command('caldav:sync')->hourly(); + + // Alternativen: + // $schedule->command('caldav:sync')->daily(); + // $schedule->command('caldav:sync --calendar=/calendars/me/default/')->dailyAt('02:00'); + } + + /** + * Register the commands for the application. + */ + protected function commands(): void + { + $this->load(__DIR__ . '/Commands'); + + require base_path('routes/console.php'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/TodoController.php b/app/Http/Controllers/TodoController.php new file mode 100644 index 0000000..f157de0 --- /dev/null +++ b/app/Http/Controllers/TodoController.php @@ -0,0 +1,82 @@ +get(); + return ApiDataTransformer::snakeToCamel($todos->toArray()); + } + + public function show(string $id) + { + return Todo::with('type')->findOrFail($id); + } + + public function store(Request $request) + { + $data = $request->validate([ + 'id' => 'nullable|string', + 'etag' => 'nullable|string', + 'title' => 'required|string', + 'description' => 'nullable|string', + 'type_id' => 'nullable|exists:todo_types,id', + 'url' => 'nullable|url', + 'due_date' => 'nullable|date', + 'recurring' => 'nullable|string', + 'priority' => 'nullable|integer', + 'status' => 'nullable|string', + 'parent' => 'nullable|string', + 'object' => 'nullable|string', + ]); + + $data['id'] = $data['id'] ?? (string) Str::uuid(); + $now = now(); + $data['created_at'] = $data['created_at'] ?? $now; + $data['last_modified'] = $now; + + $todo = Todo::create($data); + + return response()->json($todo, 201); + } + + public function update(Request $request, string $id) + { + $todo = Todo::findOrFail($id); + + $data = $request->validate([ + 'etag' => 'nullable|string', + 'title' => 'sometimes|required|string', + 'description' => 'nullable|string', + 'type_id' => 'nullable|exists:todo_types,id', + 'url' => 'nullable|url', + 'due_date' => 'nullable|date', + 'recurring' => 'nullable|string', + 'priority' => 'nullable|integer', + 'status' => 'nullable|string', + 'parent' => 'nullable|string', + 'object' => 'nullable|string', + ]); + + $data['last_modified'] = now(); + + $todo->update($data); + + return response()->json($todo); + } + + public function destroy(string $id) + { + $todo = Todo::findOrFail($id); + $todo->delete(); + + return response()->noContent(); + } +} diff --git a/app/Models/Todo.php b/app/Models/Todo.php new file mode 100644 index 0000000..1eea1f2 --- /dev/null +++ b/app/Models/Todo.php @@ -0,0 +1,56 @@ + 'datetime', + 'created_at' => 'datetime', + 'last_modified' => 'datetime', + ]; + + public function type() + { + return $this->belongsTo(TodoType::class, 'type_id'); + } + + public function parentTodo() + { + return $this->belongsTo(self::class, 'parent'); + } + + public function children() + { + return $this->hasMany(self::class, 'parent'); + } +} \ No newline at end of file diff --git a/app/Models/TodoType.php b/app/Models/TodoType.php new file mode 100644 index 0000000..11faa32 --- /dev/null +++ b/app/Models/TodoType.php @@ -0,0 +1,20 @@ +hasMany(Todo::class, 'type_id'); + } +} \ No newline at end of file diff --git a/app/Services/CaldavService.php b/app/Services/CaldavService.php new file mode 100644 index 0000000..7d97784 --- /dev/null +++ b/app/Services/CaldavService.php @@ -0,0 +1,174 @@ +calendar = new CalendarInfo(); + $this->baseUrl = rtrim($cfg['base_url'] ?? '', '/') . '/'; + $this->userName = $cfg['username'] ?? ''; + $this->calendar->displayName = $cfg['calendar_name'] ?? ''; + + if ( + empty($this->baseUrl) || + empty($this->userName) || + empty($cfg['password'] ?? '') || + empty($this->calendar->displayName) + ) { + Log::warning('CalDAV config incomplete', ['base' => $this->baseUrl, 'calendarName' => $this->calendar->displayName, 'userName' => $this->userName]); + } + + $this->client = new Client([ + 'baseUri' => $this->baseUrl, + 'userName' => $cfg['username'] ?? null, + 'password' => $cfg['password'] ?? null, + 'authType' => \Sabre\DAV\Client::AUTH_BASIC, + 'userAgent' => $cfg['user_agent'] ?? 'caldav-client', + ]); + + $this->getCalendarInfo(); + } + + /** + * Requests all calendars of the user and gets the ID that belongs to the calendar name + */ + public function getCalendarId() + { + $response = $this->client->propFind( + 'calendars/' . $this->userName . '/', + [ + '{DAV:}resourcetype', + '{DAV:}displayname', + ], + 1 + ); + + foreach ($response as $href => $node) { + if (array_key_exists('{DAV:}displayname', $node) && strcasecmp($node['{DAV:}displayname'], $this->calendar->displayName) == 0) { + $this->calendar->url = $href; + + $parts = explode('/', trim($href, '/')); + return array_pop($parts); + } + } + } + + /** + * Request information about the calendar (ctag, sync-token, color) + */ + public function getCalendarInfo() + { + $this->calendar->id = $this->getCalendarId(); + + $response = $this->client->propFind( + 'calendars/' . $this->userName . '/' . $this->calendar->id, + [ + '{DAV:}displayname', + '{http://calendarserver.org/ns/}getctag', + '{DAV:}sync-token', + '{http://apple.com/ns/ical/}calendar-color', + ] + ); + + // TODO: store ctag to compare whether the calendar has changed + $this->calendar->ctag = $response['{http://calendarserver.org/ns/}getctag']; + $this->calendar->syncToken = $response['{DAV:}sync-token']; + $this->calendar->color = $response['{http://apple.com/ns/ical/}calendar-color']; + + return $this->calendar; + } + + public function getTodos() + { + $body = '' + . ' ' + . ' ' + . ' ' + . ' ' + . ' ' + . ' ' + . ' ' + . ' ' + . ' ' + . ''; + + $headers = [ + 'Depth' => '1', + 'Content-Type' => 'application/xml; charset=utf-8', + 'Prefer' => 'return-minimal' + ]; + + $response = $this->client->request( + 'REPORT', + 'calendars/' . $this->userName . '/' . $this->calendar->id, + $body, + $headers + ); + + + // Sabre\DAV\Xml\Service + $service = new Service(); + // Sabre\DAV\Xml\Response\MultiStatus + $multiStatus = $service->parse($response['body']); + // Sabre\DAV\Xml\Element\Response + $responses = $multiStatus->getResponses(); + + $todos = []; + foreach ($responses as $key => $response) { + $href = $response->getHref(); + $properties = $response->getResponseProperties(); + $calendarData = $properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']; + $etag = $properties[200]['{DAV:}getetag']; + $vcalendar = VObject\Reader::read($calendarData); + + // Create new todo + $todo = new Todo(); + + // Map VTODO -> App\Model\Todo + $todo->id = isset($vcalendar->VTODO->UID) ? $vcalendar->VTODO->UID->getValue() : uniqid(); + $todo->etag = $etag; + $todo->title = isset($vcalendar->VTODO->SUMMARY) ? $vcalendar->VTODO->SUMMARY->getValue() : 'Neues Todo'; + if (isset($vcalendar->VTODO->DESCRIPTION)) $todo->description = $vcalendar->VTODO->DESCRIPTION->getValue(); + $todo->type_id = 2; + $todo->url = $href; + if (isset($vcalendar->VTODO->DUE)) $todo->due_date = $vcalendar->VTODO->DUE->getDateTime(); + if(isset($vcalendar->VTODO->RRULE)) $todo->recurring = $vcalendar->VTODO->RRULE->getValue(); + if(isset($vcalendar->VTODO->PRIORITY)) $todo->priority = $vcalendar->VTODO->PRIORITY->getValue(); + if(isset($vcalendar->VTODO->STATUS)) $todo->status = $vcalendar->VTODO->STATUS->getValue(); + if(isset($vcalendar->VTODO->{'RELATED-TO'})) $todo->parent = $vcalendar->VTODO->{'RELATED-TO'}->getValue(); + $todo->created_at = $vcalendar->VTODO->CREATED->getDateTime(); + $todo->last_modified = $vcalendar->VTODO->{'LAST-MODIFIED'}->getDateTime(); + + $todos[] = $todo; + } + + return $todos; + } +} diff --git a/composer.json b/composer.json index 81f3b6a..8debe3c 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,8 @@ "laravel/sanctum": "^4.2", "laravel/tinker": "^2.10", "laravel/wayfinder": "^0.1.12", + "sabre/dav": "^4.7", + "sabre/vobject": "^4.5", "tuncaybahadir/quar": "^1.5" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 7a14566..b4c88a6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6b269e9a8e5a88ee3770bca378339d31", + "content-hash": "fb096bd73a886e7565163c92e76eed53", "packages": [ { "name": "bacon/bacon-qr-code", @@ -4954,6 +4954,451 @@ }, "time": "2025-07-11T13:20:48+00:00" }, + { + "name": "sabre/dav", + "version": "4.7.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/dav.git", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-dom": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "lib-libxml": ">=2.7.0", + "php": "^7.1.0 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "sabre/event": "^5.0", + "sabre/http": "^5.0.5", + "sabre/uri": "^2.0", + "sabre/vobject": "^4.2.1", + "sabre/xml": "^2.0.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19", + "monolog/monolog": "^1.27 || ^2.0", + "phpstan/phpstan": "^0.12 || ^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "ext-curl": "*", + "ext-imap": "*", + "ext-pdo": "*" + }, + "bin": [ + "bin/sabredav", + "bin/naturalselection" + ], + "type": "library", + "autoload": { + "psr-4": { + "Sabre\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "WebDAV Framework for PHP", + "homepage": "http://sabre.io/", + "keywords": [ + "CalDAV", + "CardDAV", + "WebDAV", + "framework", + "iCalendar" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/dav/issues", + "source": "https://github.com/fruux/sabre-dav" + }, + "time": "2024-10-29T11:46:02+00:00" + }, + { + "name": "sabre/event", + "version": "5.1.7", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/86d57e305c272898ba3c28e9bd3d65d5464587c2", + "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||^3.63", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ], + "psr-4": { + "Sabre\\Event\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/event/issues", + "source": "https://github.com/fruux/sabre-event" + }, + "time": "2024-08-27T11:23:05+00:00" + }, + { + "name": "sabre/http", + "version": "5.1.13", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/http.git", + "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/http/zipball/7c2a14097d1a0de2347dcbdc91a02f38e338f4db", + "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-curl": "*", + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/event": ">=4.0 <6.0", + "sabre/uri": "^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "ext-curl": " to make http requests with the Client class" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\HTTP\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "The sabre/http library provides utilities for dealing with http requests and responses. ", + "homepage": "https://github.com/fruux/sabre-http", + "keywords": [ + "http" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/http/issues", + "source": "https://github.com/fruux/sabre-http" + }, + "time": "2025-09-09T10:21:47+00:00" + }, + { + "name": "sabre/uri", + "version": "2.3.4", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/uri.git", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.63", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\Uri\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", + "keywords": [ + "rfc3986", + "uri", + "url" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/uri/issues", + "source": "https://github.com/fruux/sabre-uri" + }, + "time": "2024-08-27T12:18:16+00:00" + }, + { + "name": "sabre/vobject", + "version": "4.5.7", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/ff22611a53782e90c97be0d0bc4a5f98a5c0a12c", + "reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/xml": "^2.1 || ^3.0 || ^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12 || ^1.12 || ^2.0", + "phpunit/php-invoker": "^2.0 || ^3.1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "hoa/bench": "If you would like to run the benchmark scripts" + }, + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabre\\VObject\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "role": "Developer" + }, + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net", + "homepage": "http://mnt.io/", + "role": "Developer" + } + ], + "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "homepage": "http://sabre.io/vobject/", + "keywords": [ + "availability", + "freebusy", + "iCalendar", + "ical", + "ics", + "jCal", + "jCard", + "recurrence", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868", + "vCalendar", + "vCard", + "vcf", + "xCal", + "xCard" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/vobject/issues", + "source": "https://github.com/fruux/sabre-vobject" + }, + "time": "2025-04-17T09:22:48+00:00" + }, + { + "name": "sabre/xml", + "version": "2.2.11", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/xml.git", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": "^7.1 || ^8.0", + "sabre/uri": ">=1.0,<3.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ], + "psr-4": { + "Sabre\\Xml\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role": "Developer" + } + ], + "description": "sabre/xml is an XML library that you may not hate.", + "homepage": "https://sabre.io/xml/", + "keywords": [ + "XMLReader", + "XMLWriter", + "dom", + "xml" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/xml/issues", + "source": "https://github.com/fruux/sabre-xml" + }, + "time": "2024-09-06T07:37:46+00:00" + }, { "name": "setasign/fpdf", "version": "1.8.6", diff --git a/config/caldav.php b/config/caldav.php new file mode 100644 index 0000000..c36c74b --- /dev/null +++ b/config/caldav.php @@ -0,0 +1,10 @@ + env('CALDAV_BASE_URL', ''), + 'username' => env('CALDAV_USERNAME', ''), + 'password' => env('CALDAV_PASSWORD', ''), + 'calendar_path' => env('CALDAV_CALENDAR_PATH', '/'), + 'calendar_name' => env('CALDAV_CALENDAR_NAME', '/'), + 'calendar_id' => env('CALDAV_CALENDAR_ID', '/'), + 'user_agent' => env('CALDAV_USER_AGENT', 'caramel-crm-caldav/1.0'), +]; diff --git a/database/factories/TodoFactory.php b/database/factories/TodoFactory.php new file mode 100644 index 0000000..a545ff5 --- /dev/null +++ b/database/factories/TodoFactory.php @@ -0,0 +1,33 @@ +faker->dateTimeBetween('-1 month', 'now'); + return [ + 'id' => (string) Str::uuid(), + 'etag' => null, + 'title' => $this->faker->sentence(4), + 'description' => $this->faker->optional()->paragraph(), + 'type_id' => null, + 'url' => $this->faker->optional()->url(), + 'due_date' => $this->faker->optional()->dateTimeBetween('now', '+2 months'), + 'recurring' => $this->faker->optional()->regexify('RRULE:FREQ=DAILY;COUNT=\d{1,2}'), + 'priority' => $this->faker->optional()->numberBetween(1, 9), + 'status' => $this->faker->randomElement(['NEEDS-ACTION','IN-PROCESS','COMPLETED', null]), + 'created_at' => $now, + 'last_modified' => $this->faker->dateTimeBetween($now, 'now'), + 'parent' => null, + 'object' => null, + ]; + } +} \ No newline at end of file diff --git a/database/migrations/2025_11_28_000000_create_todo_table.php b/database/migrations/2025_11_28_000000_create_todo_table.php new file mode 100644 index 0000000..b51ff72 --- /dev/null +++ b/database/migrations/2025_11_28_000000_create_todo_table.php @@ -0,0 +1,32 @@ +string('id')->primary(); // ical UID + $table->string('etag')->nullable(); // ical etag + $table->string('title'); + $table->text('description')->nullable(); + $table->foreignId('type_id')->nullable()->constrained('todo_types')->nullOnDelete(); + $table->string('url')->nullable(); + $table->timestamp('due_date')->nullable(); + $table->text('recurring')->nullable(); // RRULE + $table->tinyInteger('priority')->nullable(); + $table->string('status')->nullable(); // e.g. IN-PROCESS, NEEDS-ACTION + $table->timestamp('created_at')->nullable(); // iCal CREATED + $table->timestamp('last_modified')->nullable(); // iCal LAST-MODIFIED + $table->string('parent')->nullable()->index(); // RELATED-TO (parent UID) + $table->string('object')->nullable(); // RELATED-TO (object reference) + }); + } + + public function down(): void + { + Schema::dropIfExists('todos'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_11_28_000000_create_todo_types_table.php b/database/migrations/2025_11_28_000000_create_todo_types_table.php new file mode 100644 index 0000000..32aef78 --- /dev/null +++ b/database/migrations/2025_11_28_000000_create_todo_types_table.php @@ -0,0 +1,20 @@ +id(); + $table->string('name')->unique(); + }); + } + + public function down(): void + { + Schema::dropIfExists('todo_types'); + } +}; \ No newline at end of file diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index e3d86cb..2a191f4 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -19,7 +19,8 @@ public function run(): void { $this->call([ PaymentTermsSeeder::class, - SettingsTableSeeder::class + SettingsTableSeeder::class, + TodoTypeSeeder::class, ]); $user = User::factory()->create([ diff --git a/database/seeders/TodoTypeSeeder.php b/database/seeders/TodoTypeSeeder.php new file mode 100644 index 0000000..5d1445e --- /dev/null +++ b/database/seeders/TodoTypeSeeder.php @@ -0,0 +1,18 @@ + $name]); + } + } +} diff --git a/resources/js/pages/Dashboard.vue b/resources/js/pages/Dashboard.vue index 80d2df2..203e603 100644 --- a/resources/js/pages/Dashboard.vue +++ b/resources/js/pages/Dashboard.vue @@ -2,7 +2,7 @@ import { onMounted, ref } from "vue" import AppLayout from '@/layouts/AppLayout.vue'; -import { Trophy, ArrowRight, UserCheck2 } from 'lucide-vue-next'; +import { Trophy, ArrowRight, UserCheck2, Repeat, ClipboardCheck } from 'lucide-vue-next'; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card' import Button from '@/components/ui/crm-button/Button.vue'; @@ -13,18 +13,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/com import { Link, usePage } from '@inertiajs/vue3'; import axios, { AxiosError } from "axios"; import { toast } from "vue-sonner"; - -// defineProps<{ -// yearlyStatistics: { -// year: number, -// totalRevenue: number, -// paid: number, -// draft: number, -// issued: number, -// due: number, -// reminded: number, -// }, -// }>() +import { Todo } from "@/types"; const salesStatistics = ref({ year: new Date().getFullYear(), @@ -46,6 +35,13 @@ if (token) { onMounted(async () => { // Load sales statistics + try { + let response = await axios.get('/api/todos') + todos.value = response.data + } catch (error) { + toast.error('Fehler beim Laden der Daten', { description: (error as AxiosError).message }) + } + try { let response = await axios.get('/api/invoices/salesStatistics') salesStatistics.value = response.data @@ -55,11 +51,7 @@ onMounted(async () => { }) -const todos = ref([ - { text: 'Lorem ipsum', dueDate: '2025-11-25', done: false, type: 'phoneCall' }, - { text: 'Lorem ipsum', dueDate: '2025-11-25', done: false, type: 'mail' }, - { text: 'Lorem ipsum', dueDate: '2025-11-25', done: false, type: 'task' } -]) +const todos = ref([]) @@ -68,6 +60,76 @@ const todos = ref([
+ + +
+ + + Du hast zwei neue Erfolge + + Weiter so + + + + + + Du hast länger keine Bestandskunden mehr kontaktiert + + Hier sind ein paar Vorschläge für Dich + + + + +
+ + + + + + + Aufgaben + Card Description + + + + + +
    +
  • + +
    +
    + +
    +
    + +
    + {{ + todo.title + }} +
    +
    + + + +
    +
    + +
    + {{ toLocalDate(todo.dueDate)}} + +
    +
  • +
+ +
+
+ @@ -83,74 +145,11 @@ const todos = ref([ - - - - Aufgaben - Card Description - - -

Verspätet

-
    -
  • -
    -
    - -
    -
    - -
    - {{ todo.text - }} - - - -
    - -
    {{ toLocalDate(todo.dueDate) }}
    -
  • -
-

Heute

-
    -
  • -
    -
    - -
    -
    - -
    - {{ todo.text - }} - - - -
    - -
    {{ toLocalDate(todo.dueDate) }}
    -
  • -
-
-
- -
- + Umsatz {{ salesStatistics?.year || '' }} Card Description @@ -227,35 +226,6 @@ const todos = ref([ - - - - Neuer Erfolg - 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 - - - - - - - -
diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index ba8c554..ec20bb4 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -165,6 +165,60 @@ export function newCustomer(): Customer { } } +export interface TodoType { + id: number; + name: string; +} + +export function newTodoType(): TodoType { + return { + id: 0, + name: '' + } +} + +export interface Todo { + id: string; + etag: string | null; + title: string; + description: string | null; + typeId: number | null; + type?: TodoType | null; + url: string | null; + dueDate: Date | null; + recurring: boolean; + priority: number; + status: string; + createdAt: Date; + lastModified: Date; + parent: string | null; + parentTodo?: Todo | null; + children?: Todo[]; + object: string | null; +} + +export function newTodo(): Todo { + return { + id: '', + etag: null, + title: '', + description: null, + typeId: null, + type: null, + url: null, + dueDate: null, + recurring: false, + priority: 1, + status: 'pending', + createdAt: new Date(), + lastModified: new Date(), + parent: null, + parentTodo: null, + children: [], + object: null + } +} + export type PaymentStatus = 'draft' | 'issued' | 'paid' | 'due' | 'reminded' | 'cancelled' export interface Invoice { diff --git a/routes/api.php b/routes/api.php index 9628f2f..f753aa0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -8,7 +8,9 @@ use App\Http\Controllers\PaymentTermsController; use App\Http\Controllers\ProductController; use App\Http\Controllers\SettingController; +use App\Http\Controllers\TodoController; use App\Mail\OrderConfirmation; +use App\Services\CaldavService; Route::get('/customers/{id}', [CustomerController::class, 'single']); Route::get('/customers', [CustomerController::class, 'index']); @@ -21,9 +23,14 @@ ->defaults('modelType', 'customer') ->name('customers.notes.store'); - + Route::delete('/notes/{id}', [NoteController::class, 'delete']); +Route::get('/todo-types', function () { + return \App\Models\TodoType::all(); +}); +Route::apiResource('/todos', TodoController::class); + Route::get('/products/', [ProductController::class, 'index']); Route::get('/products/{id}', [ProductController::class, 'single']); diff --git a/routes/web.php b/routes/web.php index 6a5458d..acc2d62 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,6 +2,7 @@ use Illuminate\Support\Facades\Route; use Inertia\Inertia; +use App\Http\Controllers\TodoController; use App\Http\Controllers\InvoiceController; use App\Http\Controllers\CustomerController; use App\Http\Controllers\ProductController;