Neuer Flow (Kachel im Listen-Tab): Foto aufnehmen/waehlen, Artikelbereich per Rechteck markieren, on-device OCR (Vision, deutsch; Bild bleibt auf dem Geraet), Zeilen an /products/match schicken. Pruefen-Liste je Zeile: Artikel-Picker (Auto-Treffer ueber Schwellwert + eigene Lebensmittel-Suche), Menge (aus Zeile geparst, korrigierbar), Lagerort-Dropdown + QR-Scan. Einlagern per checkInBatch, unbekannte Zeilen werden uebersprungen; keine Neuanlage. Backend: MatchCandidate traegt package_size/base_unit mit, damit die App die Einheit ohne Extra-Abruf kennt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
539 lines
23 KiB
Swift
539 lines
23 KiB
Swift
import Foundation
|
||
|
||
enum APIError: LocalizedError {
|
||
case notConfigured
|
||
case unauthorized
|
||
case server(String)
|
||
|
||
var errorDescription: String? {
|
||
switch self {
|
||
case .notConfigured: return "Server-Adresse fehlt. Bitte in den Einstellungen hinterlegen."
|
||
case .unauthorized: return "Nicht angemeldet oder Sitzung abgelaufen."
|
||
case .server(let message): return message
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Schlanker Client für die Vorrania-REST-API.
|
||
/// Alle Pfade laufen über /api (wie im Web, siehe web/nginx.conf).
|
||
actor APIClient {
|
||
static let shared = APIClient()
|
||
|
||
private var baseURL: URL? { Session.shared.baseURL }
|
||
private var token: String? { Session.shared.token }
|
||
|
||
// MARK: - Basis
|
||
|
||
private func makeRequest(_ path: String, method: String = "GET") throws -> URLRequest {
|
||
guard let baseURL else { throw APIError.notConfigured }
|
||
guard let url = URL(string: "api" + path, relativeTo: baseURL) else {
|
||
throw APIError.notConfigured
|
||
}
|
||
var request = URLRequest(url: url)
|
||
request.httpMethod = method
|
||
request.timeoutInterval = 15
|
||
if let token { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") }
|
||
return request
|
||
}
|
||
|
||
private func send<T: Decodable>(_ request: URLRequest, as type: T.Type) async throws -> T {
|
||
let (data, response) = try await URLSession.shared.data(for: request)
|
||
try check(response, data: data)
|
||
return try JSONDecoder().decode(T.self, from: data)
|
||
}
|
||
|
||
private func check(_ response: URLResponse, data: Data) throws {
|
||
guard let http = response as? HTTPURLResponse else { return }
|
||
guard !(200..<300).contains(http.statusCode) else { return }
|
||
if http.statusCode == 401 { throw APIError.unauthorized }
|
||
// FastAPI liefert Fehler als {"detail": "..."}
|
||
if let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||
let detail = object["detail"] as? String {
|
||
throw APIError.server(detail)
|
||
}
|
||
throw APIError.server("Serverfehler (\(http.statusCode))")
|
||
}
|
||
|
||
private func jsonBody<T: Encodable>(_ request: inout URLRequest, _ value: T) throws {
|
||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||
request.httpBody = try JSONEncoder().encode(value)
|
||
}
|
||
|
||
// MARK: - Auth
|
||
|
||
func login(username: String, password: String) async throws -> TokenResponse {
|
||
var request = try makeRequest("/auth/login", method: "POST")
|
||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||
var components = URLComponents()
|
||
components.queryItems = [
|
||
URLQueryItem(name: "username", value: username),
|
||
URLQueryItem(name: "password", value: password),
|
||
]
|
||
request.httpBody = components.percentEncodedQuery?.data(using: .utf8)
|
||
return try await send(request, as: TokenResponse.self)
|
||
}
|
||
|
||
func me() async throws -> MeResponse {
|
||
try await send(try makeRequest("/auth/me"), as: MeResponse.self)
|
||
}
|
||
|
||
// MARK: - Stammdaten
|
||
|
||
func units() async throws -> [Unit] {
|
||
try await send(try makeRequest("/units"), as: [Unit].self)
|
||
}
|
||
|
||
func locations() async throws -> [StorageLocation] {
|
||
try await send(try makeRequest("/locations"), as: [StorageLocation].self)
|
||
}
|
||
|
||
/// `categoryId == 0` liefert Artikel ohne Kategorie; eine echte ID schliesst
|
||
/// die Unterkategorien mit ein.
|
||
func searchProducts(_ query: String, categoryId: Int? = nil) async throws -> [Product] {
|
||
var parts: [String] = []
|
||
if !query.isEmpty {
|
||
let escaped = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
||
parts.append("q=\(escaped)")
|
||
}
|
||
if let categoryId { parts.append("category_id=\(categoryId)") }
|
||
let suffix = parts.isEmpty ? "" : "?" + parts.joined(separator: "&")
|
||
return try await send(try makeRequest("/products" + suffix), as: [Product].self)
|
||
}
|
||
|
||
func product(id: Int) async throws -> Product {
|
||
try await send(try makeRequest("/products/\(id)"), as: Product.self)
|
||
}
|
||
|
||
/// Alles, was an einem Lagerort (inkl. Unterorte) liegt – Ziel des /l/-QR beim Nachschlagen.
|
||
func locationContents(code: String) async throws -> LocationContents {
|
||
try await send(try makeRequest("/location/\(code)"), as: LocationContents.self)
|
||
}
|
||
|
||
func lookup(barcode: String) async throws -> LookupResult {
|
||
let escaped = barcode.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
||
return try await send(try makeRequest("/products/lookup?barcode=\(escaped)"), as: LookupResult.self)
|
||
}
|
||
|
||
func createProduct(_ payload: NewProductRequest) async throws -> Product {
|
||
var request = try makeRequest("/products", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: Product.self)
|
||
}
|
||
|
||
func updateProduct(id: Int, _ payload: ProductUpdateRequest) async throws -> Product {
|
||
var request = try makeRequest("/products/\(id)", method: "PATCH")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: Product.self)
|
||
}
|
||
|
||
/// Mindestbestände je Lagerort ersetzen (Menge in Artikeleinheiten).
|
||
func setProductLocationMinStock(id: Int, _ list: [LocationMinStockIn]) async throws -> Product {
|
||
var request = try makeRequest("/products/\(id)/location-min-stock", method: "PUT")
|
||
try jsonBody(&request, list)
|
||
return try await send(request, as: Product.self)
|
||
}
|
||
|
||
/// Aktuelles Artikelfoto laden (mit Anmeldung). Gibt nil bei 404 zurück.
|
||
func productImage(id: Int) async throws -> Data? {
|
||
let request = try makeRequest("/products/\(id)/image")
|
||
let (data, response) = try await URLSession.shared.data(for: request)
|
||
if let http = response as? HTTPURLResponse, http.statusCode == 404 { return nil }
|
||
try check(response, data: data)
|
||
return data
|
||
}
|
||
|
||
/// Eigenes Foto (Kamera/Galerie) als multipart hochladen.
|
||
func uploadProductImage(id: Int, data: Data, contentType: String) async throws -> Product {
|
||
var request = try makeRequest("/products/\(id)/image", method: "PUT")
|
||
let boundary = "Boundary-\(UUID().uuidString)"
|
||
request.setValue("multipart/form-data; boundary=\(boundary)",
|
||
forHTTPHeaderField: "Content-Type")
|
||
let ext = contentType == "image/png" ? "png" : "jpg"
|
||
var body = Data()
|
||
body.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"foto.\(ext)\"\r\n"
|
||
.data(using: .utf8)!)
|
||
body.append("Content-Type: \(contentType)\r\n\r\n".data(using: .utf8)!)
|
||
body.append(data)
|
||
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
|
||
request.httpBody = body
|
||
return try await send(request, as: Product.self)
|
||
}
|
||
|
||
func deleteProductImage(id: Int) async throws {
|
||
try await sendNoContent(try makeRequest("/products/\(id)/image", method: "DELETE"))
|
||
}
|
||
|
||
// MARK: - Einzelstücke (Items mit UID/QR)
|
||
|
||
func items(productId: Int) async throws -> [Item] {
|
||
try await send(try makeRequest("/products/\(productId)/items"), as: [Item].self)
|
||
}
|
||
|
||
/// Alle Einzelstücke über alle Produkte – für die Einzelstück-Liste.
|
||
func allItems() async throws -> [Item] {
|
||
try await send(try makeRequest("/items"), as: [Item].self)
|
||
}
|
||
|
||
func createItems(productId: Int, _ payload: ItemCreateRequest) async throws -> [Item] {
|
||
var request = try makeRequest("/products/\(productId)/items", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: [Item].self)
|
||
}
|
||
|
||
/// QR-Auflösung: UID → Einzelstück.
|
||
func itemByUid(uid: String) async throws -> Item {
|
||
let escaped = uid.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? uid
|
||
return try await send(try makeRequest("/items/by-uid/\(escaped)"), as: Item.self)
|
||
}
|
||
|
||
func updateItem(id: Int, _ payload: ItemUpdateRequest) async throws -> Item {
|
||
var request = try makeRequest("/items/\(id)", method: "PATCH")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: Item.self)
|
||
}
|
||
|
||
func removeItem(id: Int, _ payload: ItemRemoveRequest) async throws {
|
||
var request = try makeRequest("/items/\(id)/remove", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
try await sendNoContent(request)
|
||
}
|
||
|
||
func uploadItemDocument(itemId: Int, data: Data, filename: String,
|
||
contentType: String) async throws -> ItemDocumentUpload {
|
||
var request = try makeRequest("/items/\(itemId)/documents", method: "POST")
|
||
let boundary = "Boundary-\(UUID().uuidString)"
|
||
request.setValue("multipart/form-data; boundary=\(boundary)",
|
||
forHTTPHeaderField: "Content-Type")
|
||
let safeName = filename.replacingOccurrences(of: "\"", with: "")
|
||
var body = Data()
|
||
body.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(safeName)\"\r\n"
|
||
.data(using: .utf8)!)
|
||
body.append("Content-Type: \(contentType)\r\n\r\n".data(using: .utf8)!)
|
||
body.append(data)
|
||
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
|
||
request.httpBody = body
|
||
return try await send(request, as: ItemDocumentUpload.self)
|
||
}
|
||
|
||
/// Beleg nur analysieren (nichts speichern) – zum Vorbefüllen beim Anlegen.
|
||
func analyzeItemDocument(data: Data, filename: String,
|
||
contentType: String) async throws -> DocumentSuggestions {
|
||
var request = try makeRequest("/items/analyze-document", method: "POST")
|
||
let boundary = "Boundary-\(UUID().uuidString)"
|
||
request.setValue("multipart/form-data; boundary=\(boundary)",
|
||
forHTTPHeaderField: "Content-Type")
|
||
let safeName = filename.replacingOccurrences(of: "\"", with: "")
|
||
var body = Data()
|
||
body.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(safeName)\"\r\n"
|
||
.data(using: .utf8)!)
|
||
body.append("Content-Type: \(contentType)\r\n\r\n".data(using: .utf8)!)
|
||
body.append(data)
|
||
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
|
||
request.httpBody = body
|
||
return try await send(request, as: DocumentSuggestions.self)
|
||
}
|
||
|
||
func itemDocumentData(itemId: Int, docId: Int) async throws -> Data {
|
||
let request = try makeRequest("/items/\(itemId)/documents/\(docId)")
|
||
let (data, response) = try await URLSession.shared.data(for: request)
|
||
try check(response, data: data)
|
||
return data
|
||
}
|
||
|
||
func deleteItemDocument(itemId: Int, docId: Int) async throws {
|
||
try await sendNoContent(
|
||
try makeRequest("/items/\(itemId)/documents/\(docId)", method: "DELETE"))
|
||
}
|
||
|
||
func deleteItem(id: Int) async throws {
|
||
try await sendNoContent(try makeRequest("/items/\(id)", method: "DELETE"))
|
||
}
|
||
|
||
func groups() async throws -> [GroupItem] {
|
||
try await send(try makeRequest("/groups"), as: [GroupItem].self)
|
||
}
|
||
|
||
func categories() async throws -> [CategoryItem] {
|
||
try await send(try makeRequest("/categories"), as: [CategoryItem].self)
|
||
}
|
||
|
||
func createGroup(_ payload: NewGroupRequest) async throws -> GroupItem {
|
||
var request = try makeRequest("/groups", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: GroupItem.self)
|
||
}
|
||
|
||
func settings() async throws -> [SettingEntry] {
|
||
try await send(try makeRequest("/settings"), as: [SettingEntry].self)
|
||
}
|
||
|
||
// MARK: - Listen
|
||
|
||
func shoppingList() async throws -> [ShoppingItem] {
|
||
try await send(try makeRequest("/shopping-list"), as: [ShoppingItem].self)
|
||
}
|
||
|
||
func shoppingGroups() async throws -> [GroupShoppingItem] {
|
||
try await send(try makeRequest("/shopping-list/groups"), as: [GroupShoppingItem].self)
|
||
}
|
||
|
||
func shoppingByLocation() async throws -> [LocationNeeds] {
|
||
try await send(try makeRequest("/shopping-list/by-location"), as: [LocationNeeds].self)
|
||
}
|
||
|
||
func expiring(days: Int? = nil) async throws -> [ExpiringItem] {
|
||
let path = days.map { "/expiring?days=\($0)" } ?? "/expiring"
|
||
return try await send(try makeRequest(path), as: [ExpiringItem].self)
|
||
}
|
||
|
||
// MARK: - Bestand
|
||
|
||
func lots(productId: Int) async throws -> [Lot] {
|
||
try await send(try makeRequest("/lots?product_id=\(productId)"), as: [Lot].self)
|
||
}
|
||
|
||
func checkInBatch(_ payload: BatchCheckInRequest) async throws -> StockResponse {
|
||
var request = try makeRequest("/stock/checkin/batch", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: StockResponse.self)
|
||
}
|
||
|
||
/// Kassenzettel-Zeilen (OCR) den aehnlichsten Lebensmitteln zuordnen.
|
||
func matchReceipt(_ lines: [String], threshold: Int? = nil) async throws -> [MatchLine] {
|
||
var request = try makeRequest("/products/match", method: "POST")
|
||
try jsonBody(&request, MatchRequest(lines: lines, threshold: threshold))
|
||
return try await send(request, as: [MatchLine].self)
|
||
}
|
||
|
||
func checkOut(_ payload: CheckOutRequest) async throws -> StockResponse {
|
||
var request = try makeRequest("/stock/checkout", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: StockResponse.self)
|
||
}
|
||
|
||
func updateLot(id: Int, _ payload: LotUpdateRequest) async throws -> Lot {
|
||
var request = try makeRequest("/lots/\(id)", method: "PATCH")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: Lot.self)
|
||
}
|
||
|
||
func deleteLot(id: Int) async throws {
|
||
try await sendNoContent(try makeRequest("/lots/\(id)", method: "DELETE"))
|
||
}
|
||
|
||
/// Fuer Antworten ohne Rumpf (204) - dort gibt es nichts zu dekodieren.
|
||
private func sendNoContent(_ request: URLRequest) async throws {
|
||
let (data, response) = try await URLSession.shared.data(for: request)
|
||
try check(response, data: data)
|
||
}
|
||
|
||
// MARK: - Gegenstaende (Non-Food)
|
||
|
||
func shops() async throws -> [ShopItem] {
|
||
try await send(try makeRequest("/shops"), as: [ShopItem].self)
|
||
}
|
||
|
||
func createShop(_ payload: NewShopRequest) async throws -> ShopItem {
|
||
var request = try makeRequest("/shops", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: ShopItem.self)
|
||
}
|
||
|
||
func updateShop(id: Int, _ payload: ShopUpdateRequest) async throws -> ShopItem {
|
||
var request = try makeRequest("/shops/\(id)", method: "PATCH")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: ShopItem.self)
|
||
}
|
||
|
||
func deleteShop(id: Int) async throws {
|
||
try await sendNoContent(try makeRequest("/shops/\(id)", method: "DELETE"))
|
||
}
|
||
|
||
/// Effektive (vererbte) Felder einer Kategorie – fuer das Artikelformular.
|
||
func categoryFields(categoryId: Int) async throws -> [FieldDefinition] {
|
||
try await send(try makeRequest("/categories/\(categoryId)/fields"), as: [FieldDefinition].self)
|
||
}
|
||
|
||
/// Nur die *eigenen* Felder einer Kategorie (ohne vererbte) – fuer die
|
||
/// Feldverwaltung, wo man sie anlegt, bearbeitet und loescht.
|
||
func ownFieldDefinitions(categoryId: Int) async throws -> [FieldDefinition] {
|
||
try await send(try makeRequest("/field-definitions?category_id=\(categoryId)"),
|
||
as: [FieldDefinition].self)
|
||
}
|
||
|
||
func createFieldDefinition(_ payload: FieldDefinitionCreate) async throws -> FieldDefinition {
|
||
var request = try makeRequest("/field-definitions", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: FieldDefinition.self)
|
||
}
|
||
|
||
func updateFieldDefinition(id: Int, _ payload: FieldDefinitionUpdate) async throws -> FieldDefinition {
|
||
var request = try makeRequest("/field-definitions/\(id)", method: "PATCH")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: FieldDefinition.self)
|
||
}
|
||
|
||
func deleteFieldDefinition(id: Int) async throws {
|
||
try await sendNoContent(try makeRequest("/field-definitions/\(id)", method: "DELETE"))
|
||
}
|
||
|
||
/// Menge eines Gegenstands an einem Lagerort erhoehen.
|
||
func objectCheckIn(_ payload: ObjectCheckInRequest) async throws -> StockResponse {
|
||
var request = try makeRequest("/stock/checkin", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: StockResponse.self)
|
||
}
|
||
|
||
/// Menge von einem Lagerort zum anderen umbuchen (ohne Grund).
|
||
func relocate(_ payload: RelocateRequest) async throws -> StockResponse {
|
||
var request = try makeRequest("/stock/relocate", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: StockResponse.self)
|
||
}
|
||
|
||
/// Menge mit Pflicht-Grund aus dem Bestand entfernen.
|
||
func removeStock(_ payload: RemoveRequest) async throws -> StockResponse {
|
||
var request = try makeRequest("/stock/remove", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: StockResponse.self)
|
||
}
|
||
|
||
/// Entnahme-Statistik (Summe je Grund) und die juengsten Entnahmen.
|
||
func productRemovals(id: Int) async throws -> RemovalSummary {
|
||
try await send(try makeRequest("/products/\(id)/removals"), as: RemovalSummary.self)
|
||
}
|
||
|
||
// MARK: - Uebersicht und Verlauf
|
||
|
||
func dashboardStats() async throws -> DashboardStats {
|
||
try await send(try makeRequest("/dashboard/stats"), as: DashboardStats.self)
|
||
}
|
||
|
||
/// Die Dashboards des Benutzers - dieselben wie in der Web-Oberflaeche.
|
||
func dashboards() async throws -> DashboardList {
|
||
try await send(try makeRequest("/dashboard/layouts"), as: DashboardList.self)
|
||
}
|
||
|
||
// MARK: - Diagrammdaten
|
||
|
||
func expirySplit() async throws -> ExpirySplit {
|
||
try await send(try makeRequest("/dashboard/expiry-split"), as: ExpirySplit.self)
|
||
}
|
||
|
||
func byCategory() async throws -> [CategoryShare] {
|
||
try await send(try makeRequest("/dashboard/by-category"), as: [CategoryShare].self)
|
||
}
|
||
|
||
func timeline(days: Int, productId: Int? = nil) async throws -> [TimelinePoint] {
|
||
var path = "/dashboard/timeline?days=\(days)"
|
||
if let productId { path += "&product_id=\(productId)" }
|
||
return try await send(try makeRequest(path), as: [TimelinePoint].self)
|
||
}
|
||
|
||
func activity(days: Int) async throws -> [ActivityPoint] {
|
||
try await send(try makeRequest("/dashboard/activity?days=\(days)"), as: [ActivityPoint].self)
|
||
}
|
||
|
||
/// Neueste zuerst. `productId` filtert auf einen Artikel.
|
||
func movements(limit: Int = 100, productId: Int? = nil) async throws -> [Movement] {
|
||
var path = "/movements?limit=\(limit)"
|
||
if let productId { path += "&product_id=\(productId)" }
|
||
return try await send(try makeRequest(path), as: [Movement].self)
|
||
}
|
||
|
||
// MARK: - Stammdaten
|
||
|
||
func createLocation(_ payload: NewLocationRequest) async throws -> StorageLocation {
|
||
var request = try makeRequest("/locations", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: StorageLocation.self)
|
||
}
|
||
|
||
func renameLocation(id: String, name: String) async throws -> StorageLocation {
|
||
var request = try makeRequest("/locations/\(id)", method: "PATCH")
|
||
try jsonBody(&request, RenameRequest(name: name))
|
||
return try await send(request, as: StorageLocation.self)
|
||
}
|
||
|
||
func updateLocation(id: String, _ payload: LocationUpdateRequest) async throws -> StorageLocation {
|
||
var request = try makeRequest("/locations/\(id)", method: "PATCH")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: StorageLocation.self)
|
||
}
|
||
|
||
func deleteLocation(id: String) async throws {
|
||
try await sendNoContent(try makeRequest("/locations/\(id)", method: "DELETE"))
|
||
}
|
||
|
||
func createUnit(_ payload: NewUnitRequest) async throws -> Unit {
|
||
var request = try makeRequest("/units", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: Unit.self)
|
||
}
|
||
|
||
func renameUnit(id: Int, name: String) async throws -> Unit {
|
||
var request = try makeRequest("/units/\(id)", method: "PATCH")
|
||
try jsonBody(&request, RenameRequest(name: name))
|
||
return try await send(request, as: Unit.self)
|
||
}
|
||
|
||
func deleteUnit(id: Int) async throws {
|
||
try await sendNoContent(try makeRequest("/units/\(id)", method: "DELETE"))
|
||
}
|
||
|
||
func packageTypes() async throws -> [PackageType] {
|
||
try await send(try makeRequest("/package-types"), as: [PackageType].self)
|
||
}
|
||
|
||
func createPackageType(_ payload: NewPackageTypeRequest) async throws -> PackageType {
|
||
var request = try makeRequest("/package-types", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: PackageType.self)
|
||
}
|
||
|
||
func updatePackageType(id: Int, _ payload: PackageTypeUpdateRequest) async throws -> PackageType {
|
||
var request = try makeRequest("/package-types/\(id)", method: "PATCH")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: PackageType.self)
|
||
}
|
||
|
||
func deletePackageType(id: Int) async throws {
|
||
try await sendNoContent(try makeRequest("/package-types/\(id)", method: "DELETE"))
|
||
}
|
||
|
||
func createCategory(_ payload: NewCategoryRequest) async throws -> CategoryItem {
|
||
var request = try makeRequest("/categories", method: "POST")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: CategoryItem.self)
|
||
}
|
||
|
||
func renameCategory(id: Int, name: String) async throws -> CategoryItem {
|
||
var request = try makeRequest("/categories/\(id)", method: "PATCH")
|
||
try jsonBody(&request, RenameRequest(name: name))
|
||
return try await send(request, as: CategoryItem.self)
|
||
}
|
||
|
||
func updateCategory(id: Int, _ payload: CategoryUpdateRequest) async throws -> CategoryItem {
|
||
var request = try makeRequest("/categories/\(id)", method: "PATCH")
|
||
try jsonBody(&request, payload)
|
||
return try await send(request, as: CategoryItem.self)
|
||
}
|
||
|
||
func deleteCategory(id: Int) async throws {
|
||
try await sendNoContent(try makeRequest("/categories/\(id)", method: "DELETE"))
|
||
}
|
||
|
||
func renameGroup(id: Int, name: String) async throws -> GroupItem {
|
||
var request = try makeRequest("/groups/\(id)", method: "PATCH")
|
||
try jsonBody(&request, RenameRequest(name: name))
|
||
return try await send(request, as: GroupItem.self)
|
||
}
|
||
|
||
func deleteGroup(id: Int) async throws {
|
||
try await sendNoContent(try makeRequest("/groups/\(id)", method: "DELETE"))
|
||
}
|
||
}
|