diff --git a/api.yaml b/api.yaml new file mode 100644 index 0000000..b03ac49 --- /dev/null +++ b/api.yaml @@ -0,0 +1,893 @@ +openapi: 3.0.3 +info: + title: KidsPOS API + description: 子供向けPOSシステムのREST API + version: 1.0.0 + contact: + name: KidsPOS Team + url: https://github.com/KidsPOSProject/KidsPOS-Server + +servers: + - url: http://localhost:8080 + description: Local development server + - url: https://api.kidspos.example.com + description: Production server + +tags: + - name: Items + description: 商品管理 + - name: Sales + description: 売上管理 + - name: Stores + description: 店舗管理 + - name: Settings + description: 設定管理 + +paths: + # Items + /api/item: + get: + tags: + - Items + summary: 商品一覧取得 + description: 登録されている全商品を取得します + operationId: getAllItems + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ItemResponse' + + post: + tags: + - Items + summary: 商品登録 + description: 新しい商品を登録します + operationId: createItem + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateItemRequest' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/ItemResponse' + '400': + description: Invalid request + '409': + description: Barcode already exists + + /api/item/{id}: + get: + tags: + - Items + summary: 商品取得 + description: 指定したIDの商品を取得します + operationId: getItemById + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ItemResponse' + '404': + description: Item not found + + put: + tags: + - Items + summary: 商品更新 + description: 指定したIDの商品を更新します + operationId: updateItem + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateItemRequest' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ItemResponse' + '404': + description: Item not found + + patch: + tags: + - Items + summary: 商品部分更新 + description: 指定したIDの商品を部分的に更新します + operationId: partialUpdateItem + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + barcode: + type: string + name: + type: string + price: + type: integer + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ItemResponse' + '404': + description: Item not found + + delete: + tags: + - Items + summary: 商品削除 + description: 指定したIDの商品を削除します + operationId: deleteItem + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + '404': + description: Item not found + + /api/item/barcode/{barcode}: + get: + tags: + - Items + summary: バーコードで商品取得 + description: バーコードから商品を検索します + operationId: getItemByBarcode + parameters: + - name: barcode + in: path + required: true + schema: + type: string + pattern: '^[0-9]{4,}$' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ItemResponse' + '400': + description: Invalid barcode format + '404': + description: Item not found + + /api/item/barcode-pdf: + get: + tags: + - Items + summary: バーコードPDF生成 + description: 全商品のバーコードをPDF形式で生成します + operationId: generateBarcodePdf + responses: + '200': + description: Success + content: + application/pdf: + schema: + type: string + format: binary + + # Sales + /api/sales: + get: + tags: + - Sales + summary: 売上一覧取得 + description: 全売上データを取得します + operationId: getAllSales + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SaleResponse' + + post: + tags: + - Sales + summary: 売上登録 + description: 新しい売上を登録します + operationId: createSale + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSaleRequest' + responses: + '201': + description: Created + content: + application/json: + schema: + type: object + properties: + id: + type: integer + description: 売上ID + amount: + type: integer + description: 合計金額 + quantity: + type: integer + description: 商品数 + deposit: + type: integer + description: 預かり金額 + change: + type: integer + description: おつり + storeId: + type: integer + description: 店舗ID + '400': + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Processing error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/sales/{id}: + get: + tags: + - Sales + summary: 売上詳細取得 + description: 指定したIDの売上詳細を取得します + operationId: getSaleById + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/SaleResponse' + '404': + description: Sale not found + + /api/sales/validate-printer/{storeId}: + get: + tags: + - Sales + summary: プリンター設定確認 + description: 指定した店舗のプリンター設定を確認します + operationId: validatePrinter + parameters: + - name: storeId + in: path + required: true + schema: + type: integer + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + isValid: + type: boolean + + # Stores + /api/stores: + get: + tags: + - Stores + summary: 店舗一覧取得 + description: 全店舗を取得します + operationId: getAllStores + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/StoreEntity' + + post: + tags: + - Stores + summary: 店舗登録 + description: 新しい店舗を登録します + operationId: createStore + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StoreEntity' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/StoreEntity' + + /api/stores/{id}: + get: + tags: + - Stores + summary: 店舗取得 + description: 指定したIDの店舗を取得します + operationId: getStoreById + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/StoreEntity' + '404': + description: Store not found + + put: + tags: + - Stores + summary: 店舗更新 + description: 店舗情報を更新します + operationId: updateStore + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StoreEntity' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/StoreEntity' + '404': + description: Store not found + + delete: + tags: + - Stores + summary: 店舗削除 + description: 店舗を削除します + operationId: deleteStore + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + '404': + description: Store not found + + # Settings + /api/setting: + get: + tags: + - Settings + summary: 設定一覧取得 + description: 全設定を取得します + operationId: getAllSettings + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SettingEntity' + + post: + tags: + - Settings + summary: 設定作成 + description: 新しい設定を作成します + operationId: createSetting + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SettingEntity' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/SettingEntity' + + /api/setting/status: + get: + tags: + - Settings + summary: ステータス取得 + description: APIのステータスを取得します + operationId: getStatus + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "OK" + + /api/setting/{key}: + get: + tags: + - Settings + summary: 設定取得 + description: 指定したキーの設定を取得します + operationId: getSettingByKey + parameters: + - name: key + in: path + required: true + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/SettingEntity' + '404': + description: Setting not found + + put: + tags: + - Settings + summary: 設定更新 + description: 設定を更新します + operationId: updateSetting + parameters: + - name: key + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + value: + type: string + required: + - value + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/SettingEntity' + '404': + description: Setting not found + + delete: + tags: + - Settings + summary: 設定削除 + description: 設定を削除します + operationId: deleteSetting + parameters: + - name: key + in: path + required: true + schema: + type: string + responses: + '204': + description: No Content + '404': + description: Setting not found + + /api/setting/printer/{storeId}: + get: + tags: + - Settings + summary: プリンター設定取得 + description: 店舗のプリンター設定を取得します + operationId: getPrinterSettings + parameters: + - name: storeId + in: path + required: true + schema: + type: integer + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + storeId: + type: integer + host: + type: string + port: + type: integer + '404': + description: Printer settings not found + + post: + tags: + - Settings + summary: プリンター設定保存 + description: 店舗のプリンター設定を保存します + operationId: savePrinterSettings + parameters: + - name: storeId + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + host: + type: string + port: + type: integer + required: + - host + - port + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + storeId: + type: integer + host: + type: string + port: + type: integer + message: + type: string + + /api/setting/application: + get: + tags: + - Settings + summary: アプリケーション設定取得 + description: アプリケーション全体の設定を取得します + operationId: getApplicationSettings + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationSetting' + '404': + description: Application settings not found + + post: + tags: + - Settings + summary: アプリケーション設定保存 + description: アプリケーション全体の設定を保存します + operationId: saveApplicationSettings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationSetting' + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + serverHost: + type: string + serverPort: + type: integer + message: + type: string + + +components: + schemas: + ItemResponse: + type: object + properties: + id: + type: integer + example: 1 + barcode: + type: string + example: "1234567890" + name: + type: string + example: "ポテトチップス" + price: + type: integer + example: 150 + description: 価格(リバー) + required: + - id + - barcode + - name + - price + + CreateItemRequest: + type: object + properties: + barcode: + type: string + minLength: 4 + example: "1234567890" + name: + type: string + minLength: 1 + maxLength: 100 + example: "ポテトチップス" + price: + type: integer + minimum: 0 + maximum: 99999 + example: 150 + required: + - barcode + - name + - price + + SaleResponse: + type: object + properties: + id: + type: integer + description: 売上ID + storeId: + type: integer + description: 店舗ID + storeName: + type: string + description: 店舗名 + totalAmount: + type: integer + description: 合計金額(リバー) + deposit: + type: integer + description: 預り金(リバー) + change: + type: integer + description: お釣り(リバー) + saleTime: + type: string + format: date-time + description: 売上時刻 + items: + type: array + description: 売上商品リスト + items: + $ref: '#/components/schemas/SaleItemResponse' + required: + - id + - storeId + - storeName + - totalAmount + - deposit + - change + - saleTime + + SaleItemResponse: + type: object + properties: + itemId: + type: integer + description: 商品ID + itemName: + type: string + description: 商品名 + barcode: + type: string + description: バーコード + quantity: + type: integer + description: 数量 + unitPrice: + type: integer + description: 単価(リバー) + subtotal: + type: integer + description: 小計(リバー) + required: + - itemId + - itemName + - barcode + - quantity + - unitPrice + - subtotal + + CreateSaleRequest: + type: object + properties: + storeId: + type: integer + description: 店舗ID + itemIds: + type: string + description: 商品IDのカンマ区切り文字列(例:"1,2,3") + example: "1,2,3" + deposit: + type: integer + minimum: 0 + description: 預かり金額 + required: + - storeId + - itemIds + - deposit + + + StoreEntity: + type: object + properties: + id: + type: integer + name: + type: string + printerUri: + type: string + nullable: true + required: + - name + + SettingEntity: + type: object + properties: + key: + type: string + value: + type: string + required: + - key + - value + + ApplicationSetting: + type: object + properties: + serverHost: + type: string + serverPort: + type: integer + required: + - serverHost + - serverPort + + ErrorResponse: + type: object + properties: + code: + type: string + description: エラーコード + example: "VALIDATION_ERROR" + message: + type: string + description: エラーメッセージ + example: "商品IDが空です。売上作成には少なくとも1つの商品が必要です" + timestamp: + type: string + format: date-time + description: エラー発生時刻 + path: + type: string + nullable: true + description: リクエストパス + details: + type: object + nullable: true + description: 追加の詳細情報 + additionalProperties: true + required: + - code + - message + - timestamp + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + +# セキュリティは現在未実装のため、コメントアウト +# security: +# - bearerAuth: [] diff --git a/app/openapi/api.yaml b/app/openapi/api.yaml index 8ef708e..49c9917 100644 --- a/app/openapi/api.yaml +++ b/app/openapi/api.yaml @@ -18,14 +18,10 @@ tags: description: 商品管理 - name: Sales description: 売上管理 - - name: Staff - description: スタッフ管理 - name: Stores description: 店舗管理 - name: Settings description: 設定管理 - - name: Users - description: ユーザー管理 paths: # Items @@ -247,27 +243,43 @@ paths: schema: $ref: '#/components/schemas/CreateSaleRequest' responses: - '200': - description: Success + '201': + description: Created content: application/json: schema: type: object properties: - status: - type: string - saleId: + id: type: integer - totalAmount: + description: 売上ID + amount: type: integer + description: 合計金額 + quantity: + type: integer + description: 商品数 + deposit: + type: integer + description: 預かり金額 change: type: integer - receiptPrinted: - type: boolean + description: おつり + storeId: + type: integer + description: 店舗ID '400': description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' '500': description: Processing error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /api/sales/{id}: get: @@ -316,113 +328,6 @@ paths: isValid: type: boolean - # Staff - /api/staff: - get: - tags: - - Staff - summary: スタッフ一覧取得 - description: 全スタッフを取得します - operationId: getAllStaff - responses: - '200': - description: Success - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/StaffEntity' - - post: - tags: - - Staff - summary: スタッフ登録 - description: 新しいスタッフを登録します - operationId: createStaff - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateStaffRequest' - responses: - '201': - description: Created - content: - application/json: - schema: - $ref: '#/components/schemas/StaffEntity' - - /api/staff/{barcode}: - get: - tags: - - Staff - summary: スタッフ取得 - description: バーコードからスタッフを取得します - operationId: getStaffByBarcode - parameters: - - name: barcode - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/StaffEntity' - '404': - description: Staff not found - - put: - tags: - - Staff - summary: スタッフ更新 - description: スタッフ情報を更新します - operationId: updateStaff - parameters: - - name: barcode - in: path - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateStaffRequest' - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/StaffEntity' - '404': - description: Staff not found - - delete: - tags: - - Staff - summary: スタッフ削除 - description: スタッフを削除します - operationId: deleteStaff - parameters: - - name: barcode - in: path - required: true - schema: - type: string - responses: - '204': - description: No Content - '404': - description: Staff not found - # Stores /api/stores: get: @@ -778,46 +683,6 @@ paths: message: type: string - # Users - /api/users: - get: - tags: - - Users - summary: ユーザー一覧取得 - description: 全ユーザーを取得します - operationId: getAllUsers - responses: - '200': - description: Success - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/StaffEntity' - - /api/users/{barcode}: - get: - tags: - - Users - summary: ユーザー取得 - description: バーコードからユーザーを取得します - operationId: getUserByBarcode - parameters: - - name: barcode - in: path - required: true - schema: - type: string - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/StaffEntity' - '404': - description: User not found components: schemas: @@ -870,102 +735,88 @@ components: properties: id: type: integer + description: 売上ID storeId: type: integer - staffBarcode: - type: string - createdAt: + description: 店舗ID + storeName: type: string - format: date-time + description: 店舗名 totalAmount: type: integer description: 合計金額(リバー) deposit: type: integer description: 預り金(リバー) - items: - type: array - items: - type: object - properties: - barcode: - type: string - name: - type: string - price: - type: integer - quantity: - type: integer - - CreateSaleRequest: - type: object - properties: - storeId: + change: type: integer - staffBarcode: + description: お釣り(リバー) + saleTime: type: string + format: date-time + description: 売上時刻 items: type: array + description: 売上商品リスト items: - type: object - properties: - barcode: - type: string - quantity: - type: integer - minimum: 1 - required: - - barcode - - quantity - deposit: - type: integer - minimum: 0 - printReceipt: - type: boolean - default: true + $ref: '#/components/schemas/SaleItemResponse' required: + - id - storeId - - staffBarcode - - items + - storeName + - totalAmount - deposit + - change + - saleTime - StaffEntity: + SaleItemResponse: type: object properties: - barcode: - type: string - example: "STAFF001" - name: + itemId: + type: integer + description: 商品ID + itemName: type: string - example: "田中太郎" - required: - - barcode - - name - - CreateStaffRequest: - type: object - properties: + description: 商品名 barcode: type: string - minLength: 1 - maxLength: 50 - name: - type: string - minLength: 1 - maxLength: 100 + description: バーコード + quantity: + type: integer + description: 数量 + unitPrice: + type: integer + description: 単価(リバー) + subtotal: + type: integer + description: 小計(リバー) required: + - itemId + - itemName - barcode - - name + - quantity + - unitPrice + - subtotal - UpdateStaffRequest: + CreateSaleRequest: type: object properties: - name: + storeId: + type: integer + description: 店舗ID + itemIds: type: string - minLength: 1 - maxLength: 100 + description: 商品IDのカンマ区切り文字列(例:"1,2,3") + example: "1,2,3" + deposit: + type: integer + minimum: 0 + description: 預かり金額 required: - - name + - storeId + - itemIds + - deposit + StoreEntity: type: object @@ -1005,15 +856,30 @@ components: ErrorResponse: type: object properties: - error: + code: type: string + description: エラーコード + example: "VALIDATION_ERROR" message: type: string + description: エラーメッセージ + example: "商品IDが空です。売上作成には少なくとも1つの商品が必要です" timestamp: type: string format: date-time + description: エラー発生時刻 path: type: string + nullable: true + description: リクエストパス + details: + type: string + nullable: true + description: 追加の詳細情報(JSON文字列) + required: + - code + - message + - timestamp securitySchemes: bearerAuth: @@ -1023,4 +889,4 @@ components: # セキュリティは現在未実装のため、コメントアウト # security: -# - bearerAuth: [] \ No newline at end of file +# - bearerAuth: [] diff --git a/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt b/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt index cd0ff66..ad4917b 100644 --- a/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt +++ b/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt @@ -3,7 +3,6 @@ package info.nukoneko.cuc.android.kidspos.di.module import info.nukoneko.cuc.android.kidspos.api.APIService import info.nukoneko.cuc.android.kidspos.entity.Item import info.nukoneko.cuc.android.kidspos.entity.Sale -import info.nukoneko.cuc.android.kidspos.entity.Staff import info.nukoneko.cuc.android.kidspos.entity.Store import org.koin.dsl.module import java.util.* @@ -15,7 +14,6 @@ import java.util.* class DemoAPIService : APIService( itemsApi = throw NotImplementedError("Demo mode - itemsApi not used"), salesApi = throw NotImplementedError("Demo mode - salesApi not used"), - staffApi = throw NotImplementedError("Demo mode - staffApi not used"), storesApi = throw NotImplementedError("Demo mode - storesApi not used"), settingsApi = throw NotImplementedError("Demo mode - settingsApi not used") ) { @@ -23,7 +21,6 @@ class DemoAPIService : APIService( override suspend fun createSale( storeId: Int, - staffBarcode: String, deposit: Int, itemIds: String ): Sale = Sale( @@ -52,9 +49,6 @@ class DemoAPIService : APIService( storeId = 1, genreId = 1 ) - - override suspend fun getStaff(staffBarcode: String): Staff = - Staff(staffBarcode, "DemoStaff") } val apiModule = module { diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/APIService.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/APIService.kt index b23e6e0..aa28b58 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/APIService.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/APIService.kt @@ -4,7 +4,6 @@ import info.nukoneko.cuc.android.kidspos.api.generated.* import info.nukoneko.cuc.android.kidspos.api.generated.model.* import info.nukoneko.cuc.android.kidspos.entity.Item import info.nukoneko.cuc.android.kidspos.entity.Sale -import info.nukoneko.cuc.android.kidspos.entity.Staff import info.nukoneko.cuc.android.kidspos.entity.Store import retrofit2.Response @@ -14,7 +13,6 @@ import retrofit2.Response open class APIService( private val itemsApi: ItemsApi, private val salesApi: SalesApi, - private val staffApi: StaffApi, private val storesApi: StoresApi, private val settingsApi: SettingsApi ) { @@ -36,38 +34,27 @@ open class APIService( open suspend fun createSale( storeId: Int, - staffBarcode: String, deposit: Int, itemIds: String ): Sale { - // itemIdsをカンマ区切りからリストに変換 - // 注意: 新しいAPIではitemIdではなくバーコードを使用 - val itemBarcodes = itemIds.split(",") - val request = CreateSaleRequest( storeId = storeId, - staffBarcode = staffBarcode, - deposit = deposit, - items = itemBarcodes.map { barcode -> - CreateSaleRequestItemsInner( - barcode = barcode, - quantity = 1 // デフォルトで1個とする - ) - } + itemIds = itemIds, + deposit = deposit ) val response = salesApi.createSale(request) return if (response.isSuccessful) { val saleResponse = response.body()!! Sale( - id = saleResponse.saleId ?: 0, - barcode = saleResponse.saleId?.toString() ?: "", // バーコードはIDから生成 - createdAt = java.util.Date().toString(), // 現在時刻を設定 - points = 0, // ポイントは新APIにない - price = saleResponse.totalAmount ?: 0, - items = itemIds, // 元のitemIdsをそのまま使用 - storeId = storeId, - staffId = 0 // スタッフIDは取得できないため仮値 + id = saleResponse.id ?: 0, + barcode = saleResponse.id?.toString() ?: "", + createdAt = java.util.Date().toString(), + points = 0, + price = saleResponse.amount ?: 0, + items = itemIds, + storeId = saleResponse.storeId ?: storeId, + staffId = 0 ) } else { throw Exception("Failed to create sale: ${response.code()}") @@ -91,19 +78,6 @@ open class APIService( } } - open suspend fun getStaff(staffBarcode: String): Staff { - val response = staffApi.getStaffByBarcode(staffBarcode) - return if (response.isSuccessful) { - val staffResponse = response.body()!! - Staff( - barcode = staffResponse.barcode ?: staffBarcode, - name = staffResponse.name ?: "" - ) - } else { - throw Exception("Failed to get staff: ${response.code()}") - } - } - open suspend fun getStatus(): Any { val response = settingsApi.getStatus() return if (response.isSuccessful) { diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/GlobalConfig.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/GlobalConfig.kt index 8338a2b..8653c36 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/GlobalConfig.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/GlobalConfig.kt @@ -3,7 +3,6 @@ package info.nukoneko.cuc.android.kidspos.di import android.content.Context import androidx.core.content.edit import androidx.preference.PreferenceManager -import info.nukoneko.cuc.android.kidspos.entity.Staff import info.nukoneko.cuc.android.kidspos.entity.Store import info.nukoneko.cuc.android.kidspos.event.EventBus import info.nukoneko.cuc.android.kidspos.event.SystemEvent @@ -54,23 +53,10 @@ class GlobalConfig(context: Context, private val eventBus: EventBus, private val eventBus.post(SystemEvent.SelectShop(value)) } - var currentStaff: Staff? - get() { - return preference.getString(KEY_STAFF, null)?.let { - return json.decodeFromString(it) - } - } - set(value) { - preference.edit { - putString(KEY_STAFF, json.encodeToString(value)) - } - } - companion object { const val KEY_SERVER_INFO = "setting_server_info" const val KEY_RUNNING_MODE = "setting_running_mode" const val DEFAULT_SERVER_INFO = "http://192.168.0.220:8080" const val KEY_STORE = "store" - const val KEY_STAFF = "staff" } } diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/ServerSelectionInterceptor.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/ServerSelectionInterceptor.kt index 47a0aaa..7fed89f 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/ServerSelectionInterceptor.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/ServerSelectionInterceptor.kt @@ -1,5 +1,6 @@ package info.nukoneko.cuc.android.kidspos.di +import com.orhanobut.logger.Logger import okhttp3.Interceptor import okhttp3.Response @@ -7,9 +8,30 @@ class ServerSelectionInterceptor(var serverAddress: String) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { var request = chain.request() if (serverAddress.isNotEmpty()) { - val url = "$serverAddress/api/${request.url.pathSegments.joinToString("/")}" + val url = "$serverAddress/${request.url.pathSegments.joinToString("/")}" request = request.newBuilder().url(url).build() } - return chain.proceed(request) + + // リクエスト情報をダンプ + Logger.d("HTTP Request: ${request.method} ${request.url}") + Logger.d(" Headers: ${request.headers}") + if (request.url.querySize > 0) { + Logger.d(" Query params: ${request.url.query}") + } + request.body?.let { body -> + val buffer = okio.Buffer() + body.writeTo(buffer) + val bodyString = buffer.readUtf8() + Logger.d(" Body: content-type=${body.contentType()}") + Logger.d(" Body content: $bodyString") + } + + val startTime = System.currentTimeMillis() + val response = chain.proceed(request) + val duration = System.currentTimeMillis() - startTime + + Logger.i("HTTP Response: ${response.code} ${request.method} ${request.url} (${duration}ms)") + + return response } } \ No newline at end of file diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/module/coreModule.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/module/coreModule.kt index d63e1d5..56d6801 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/module/coreModule.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/di/module/coreModule.kt @@ -23,8 +23,16 @@ val coreModule = module { single(named("serverSelection")) { ServerSelectionInterceptor((get().currentServerAddress)) } + single(named("logging")) { + okhttp3.logging.HttpLoggingInterceptor().apply { + level = okhttp3.logging.HttpLoggingInterceptor.Level.BODY + } + } single { - OkHttpClient.Builder().addInterceptor(get(named("serverSelection"))).build() + OkHttpClient.Builder() + .addInterceptor(get(named("serverSelection"))) + .addInterceptor(get(named("logging"))) + .build() } single { val contentType = "application/json".toMediaType() diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/entity/Staff.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/entity/Staff.kt index 551f524..6be9877 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/entity/Staff.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/entity/Staff.kt @@ -1,12 +1 @@ -package info.nukoneko.cuc.android.kidspos.entity - -import kotlinx.serialization.Serializable - -@Serializable -data class Staff(val barcode: String, val name: String) { - companion object { - fun create(barcode: String): Staff { - return Staff(barcode, "Dummy") - } - } -} +// This file is deprecated and will be removed diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/event/BarcodeEvent.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/event/BarcodeEvent.kt index 5a75277..7b98cc3 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/event/BarcodeEvent.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/event/BarcodeEvent.kt @@ -1,12 +1,9 @@ package info.nukoneko.cuc.android.kidspos.event import info.nukoneko.cuc.android.kidspos.entity.Item -import info.nukoneko.cuc.android.kidspos.entity.Staff sealed class BarcodeEvent : Event { data class ReadReceiptFailed(val error: Throwable) : BarcodeEvent() data class ReadItemSuccess(val item: Item) : BarcodeEvent() data class ReadItemFailed(val error: Throwable) : BarcodeEvent() - data class ReadStaffSuccess(val staff: Staff) : BarcodeEvent() - data class ReadStaffFailed(val error: Throwable) : BarcodeEvent() } diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/common/BaseBarcodeReadableActivity.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/common/BaseBarcodeReadableActivity.kt index e581e37..9596ba0 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/common/BaseBarcodeReadableActivity.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/common/BaseBarcodeReadableActivity.kt @@ -4,7 +4,6 @@ import android.view.KeyEvent import androidx.appcompat.app.AppCompatActivity import com.orhanobut.logger.Logger import info.nukoneko.cuc.android.kidspos.error.IllegalBarcodeException -import info.nukoneko.cuc.android.kidspos.util.BarcodeKind import info.nukoneko.cuc.android.kidspos.util.BarcodeReadDelegate abstract class BaseBarcodeReadableActivity : AppCompatActivity() { @@ -14,14 +13,14 @@ abstract class BaseBarcodeReadableActivity : AppCompatActivity() { Logger.e(e, "onReadFailed") } - override fun onReadSuccess(barcode: String, kind: BarcodeKind) { - onBarcodeInput(barcode, kind) + override fun onReadSuccess(barcode: String) { + onBarcodeInput(barcode) } } private val barcodeDelegate = BarcodeReadDelegate(onBarcodeReadListener) - abstract fun onBarcodeInput(barcode: String, prefix: BarcodeKind) + abstract fun onBarcodeInput(barcode: String) final override fun dispatchKeyEvent(event: KeyEvent): Boolean { return barcodeDelegate.onKeyEvent(event) diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainActivity.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainActivity.kt index 30180f3..9bcc897 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainActivity.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainActivity.kt @@ -16,7 +16,6 @@ import info.nukoneko.cuc.android.kidspos.ui.common.ErrorDialogFragment import info.nukoneko.cuc.android.kidspos.ui.main.itemlist.ItemListFragment import info.nukoneko.cuc.android.kidspos.ui.main.storelist.StoreListDialogFragment import info.nukoneko.cuc.android.kidspos.ui.setting.SettingActivity -import info.nukoneko.cuc.android.kidspos.util.BarcodeKind import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -33,8 +32,8 @@ class MainActivity : BaseBarcodeReadableActivity(), CoroutineScope { Toast.makeText(this@MainActivity, message, Toast.LENGTH_SHORT).show() } - override fun onNotReachableServer() { - showNotReachableErrorDialog() + override fun onNotReachableServer(errorMessage: String) { + showNotReachableErrorDialog(errorMessage) } override fun onShouldChangeTitleSuffix(titleSuffix: String) { @@ -50,8 +49,8 @@ class MainActivity : BaseBarcodeReadableActivity(), CoroutineScope { fragment.isCancelable = false fragment.show(supportFragmentManager, "changeStore") } - R.id.input_dummy_item -> onBarcodeInput("1234567890", BarcodeKind.ITEM) - R.id.input_dummy_store -> onBarcodeInput("1234567890", BarcodeKind.STAFF) + + R.id.input_dummy_item -> onBarcodeInput("1234567890") } binding.drawerLayout.closeDrawer(GravityCompat.START) true @@ -91,18 +90,19 @@ class MainActivity : BaseBarcodeReadableActivity(), CoroutineScope { override fun onResume() { super.onResume() myViewModel.onResume() + @Suppress("KotlinConstantConditions") binding.navView.menu.setGroupVisible(R.id.beta_test, ProjectSettings.DEMO_MODE) } - override fun onBarcodeInput(barcode: String, prefix: BarcodeKind) { - myViewModel.onBarcodeInput(barcode, prefix) + override fun onBarcodeInput(barcode: String) { + myViewModel.onBarcodeInput(barcode) } - private fun showNotReachableErrorDialog() { + private fun showNotReachableErrorDialog(errorMessage: String) { launch { val result = ErrorDialogFragment.showWithSuspend( supportFragmentManager, - "サーバーとの接続に失敗しました\n・ネットワーク接続を確認してください\n・設定画面で設定を確認をしてください" + errorMessage ) when (result) { ErrorDialogFragment.DialogResult.OK -> SettingActivity.createIntent(this@MainActivity) diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainViewModel.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainViewModel.kt index 5b67aaf..e6ab91d 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainViewModel.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/MainViewModel.kt @@ -1,15 +1,16 @@ +@file:Suppress("KotlinConstantConditions", "unused") + package info.nukoneko.cuc.android.kidspos.ui.main import androidx.lifecycle.ViewModel +import com.orhanobut.logger.Logger import info.nukoneko.cuc.android.kidspos.ProjectSettings import info.nukoneko.cuc.android.kidspos.api.APIService import info.nukoneko.cuc.android.kidspos.di.GlobalConfig import info.nukoneko.cuc.android.kidspos.entity.Item -import info.nukoneko.cuc.android.kidspos.entity.Staff import info.nukoneko.cuc.android.kidspos.event.BarcodeEvent import info.nukoneko.cuc.android.kidspos.event.EventBus import info.nukoneko.cuc.android.kidspos.event.SystemEvent -import info.nukoneko.cuc.android.kidspos.util.BarcodeKind import info.nukoneko.cuc.android.kidspos.util.Mode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -17,7 +18,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import java.io.IOException import kotlin.coroutines.CoroutineContext class MainViewModel( @@ -36,46 +36,22 @@ class MainViewModel( private var status: ConnectionStatus = ConnectionStatus.NOT_CONNECTED - fun onBarcodeInput(barcode: String, prefix: BarcodeKind) { - @Suppress("ConstantConditionIf") + fun onBarcodeInput(barcode: String) { + Logger.i("MainViewModel.onBarcodeInput: barcode=$barcode, demoMode=${ProjectSettings.DEMO_MODE}") + if (ProjectSettings.DEMO_MODE) { - when (prefix) { - BarcodeKind.ITEM -> { - onReadItemSuccess(Item.create(barcode)) - } - BarcodeKind.STAFF -> { - onReadStaffSuccess(Staff.create(barcode)) - } - else -> { - onReadItemSuccess(Item.create(barcode)) - onReadStaffSuccess(Staff.create(barcode)) - } - } + onReadItemSuccess(Item.create(barcode)) } else { // サーバから取得する - when (prefix) { - BarcodeKind.ITEM -> { - launch { - try { - val item = requestGetItem(barcode) - onReadItemSuccess(item) - } catch (e: Throwable) { - onReadItemFailure(e) - } - } - } - BarcodeKind.STAFF -> { - launch { - try { - val staff = requestGetStaff(barcode) - onReadStaffSuccess(staff) - } catch (e: Throwable) { - onReadStaffFailure(e) - } - } - } - BarcodeKind.SALE -> eventBus.post(BarcodeEvent.ReadReceiptFailed(IOException("まだ対応していない"))) - BarcodeKind.UNKNOWN -> { + Logger.d("Requesting item from server: $barcode") + launch { + try { + val item = requestGetItem(barcode) + Logger.i("Item received from server: ${item.name}") + onReadItemSuccess(item) + } catch (e: Throwable) { + Logger.e(e, "Failed to get item from server: $barcode") + onReadItemFailure(e) } } } @@ -101,14 +77,31 @@ class MainViewModel( launch(Dispatchers.IO) { try { api.getStatus() - status = ConnectionStatus.CONNECTED - safetyShowMessage("接続しました") + withContext(Dispatchers.Main) { + status = ConnectionStatus.CONNECTED + safetyShowMessage("接続しました") + } } catch (e: Throwable) { - launch(Dispatchers.Main) { - listener?.onNotReachableServer() + val errorMessage = when (e) { + is java.net.UnknownHostException -> + "サーバーが見つかりません。ネットワーク設定を確認してください" + + is java.net.ConnectException -> + "サーバーに接続できません。サーバーが起動しているか確認してください" + + is java.net.SocketTimeoutException -> + "接続がタイムアウトしました。ネットワーク環境を確認してください" + + is javax.net.ssl.SSLException -> + "SSL証明書エラーです。サーバー設定を確認してください" + + else -> + "接続に失敗しました: ${e.javaClass.simpleName} - ${e.message ?: "不明なエラー"}" + } + withContext(Dispatchers.Main) { status = ConnectionStatus.NOT_CONNECTED + listener?.onNotReachableServer(errorMessage) } - safetyShowMessage("接続に失敗しました") } } } @@ -122,14 +115,6 @@ class MainViewModel( eventBus.post(BarcodeEvent.ReadItemFailed(e)) } - private fun onReadStaffSuccess(staff: Staff) { - eventBus.post(BarcodeEvent.ReadStaffSuccess(staff)) - } - - private fun onReadStaffFailure(e: Throwable) { - eventBus.post(BarcodeEvent.ReadStaffFailed(e)) - } - private fun safetyShowMessage(message: String) { launch(Dispatchers.Main) { listener?.onShouldShowMessage(message) @@ -142,7 +127,6 @@ class MainViewModel( titleSuffix += " [${config.currentRunningMode.modeName}モード]" - @Suppress("ConstantConditionIf") if (ProjectSettings.DEMO_MODE) titleSuffix += " [テストモード]" listener?.onShouldChangeTitleSuffix(titleSuffix) @@ -152,10 +136,6 @@ class MainViewModel( api.getItem(barcode) } - private suspend fun requestGetStaff(barcode: String) = withContext(Dispatchers.IO) { - api.getStaff(barcode) - } - @Subscribe(threadMode = ThreadMode.MAIN) fun onSelectShopEvent(@Suppress("UNUSED_PARAMETER") event: SystemEvent.SelectShop) { updateTitle() @@ -164,7 +144,7 @@ class MainViewModel( interface Listener { fun onShouldChangeTitleSuffix(titleSuffix: String) - fun onNotReachableServer() + fun onNotReachableServer(errorMessage: String) fun onShouldShowMessage(message: String) } diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/calculate/CalculatorDialogViewModel.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/calculate/CalculatorDialogViewModel.kt index f8a0826..6a8a179 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/calculate/CalculatorDialogViewModel.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/calculate/CalculatorDialogViewModel.kt @@ -94,8 +94,9 @@ class CalculatorDialogViewModel( val ids = items.map { it.id.toString() } val joinedIds = ids.joinToString(",") api.createSale( - config.currentStore?.id ?: 0, config.currentStaff?.barcode - ?: "", deposit, joinedIds + config.currentStore?.id ?: 0, + deposit, + joinedIds ) } diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/itemlist/ItemListViewModel.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/itemlist/ItemListViewModel.kt index 1491fc1..8354352 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/itemlist/ItemListViewModel.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/itemlist/ItemListViewModel.kt @@ -23,12 +23,6 @@ class ItemListViewModel( private val currentPrice = MutableLiveData() fun getCurrentPriceText(): LiveData = currentPrice - private val currentStaff = MutableLiveData() - fun getCurrentStaffText(): LiveData = currentStaff - - private val currentStaffVisibility = MutableLiveData() - fun getCurrentStaffVisibility(): LiveData = currentStaffVisibility - private val accountButtonEnabled = MutableLiveData() fun getAccountButtonEnabled(): LiveData = accountButtonEnabled @@ -85,9 +79,6 @@ class ItemListViewModel( private fun updateViews() { currentPrice.postValue("${currentTotal()} リバー") accountButtonEnabled.postValue(data.isNotEmpty()) - currentStaffVisibility.value = - if (config.currentStaff == null) View.INVISIBLE else View.VISIBLE - currentStaff.postValue("たんとう: ${config.currentStaff?.name ?: ""}") // 練習モードのときのみ商品追加ボタンを表示 addItemButtonVisibility.postValue( if (config.currentRunningMode == Mode.PRACTICE) View.VISIBLE else View.GONE @@ -102,23 +93,11 @@ class ItemListViewModel( updateViews() } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onReadStaffSuccessEvent(event: BarcodeEvent.ReadStaffSuccess) { - val staff = event.staff - config.currentStaff = staff - updateViews() - } - @Subscribe(threadMode = ThreadMode.MAIN) fun onReadItemFailedEvent(@Suppress("UNUSED_PARAMETER") event: BarcodeEvent.ReadItemFailed) { listener?.onShouldShowMessage(R.string.request_item_failed) } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onReadStaffFailedEvent(@Suppress("UNUSED_PARAMETER") event: BarcodeEvent.ReadStaffFailed) { - listener?.onShouldShowMessage(R.string.request_staff_failed) - } - @Subscribe(threadMode = ThreadMode.MAIN) fun onReadReceiptFailedEvent(@Suppress("UNUSED_PARAMETER") event: BarcodeEvent.ReadReceiptFailed) { listener?.onShouldShowMessage(R.string.read_receipt_failed) diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/BarcodeKind.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/BarcodeKind.kt deleted file mode 100644 index 816f758..0000000 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/BarcodeKind.kt +++ /dev/null @@ -1,20 +0,0 @@ -package info.nukoneko.cuc.android.kidspos.util - -enum class BarcodeKind(val prefix: String) { - STAFF("00"), - ITEM("01"), - SALE("02"), - UNKNOWN("99"); - - companion object { - fun prefixOf(prefix: String): BarcodeKind { - for (barcodePrefix in BarcodeKind.values()) { - if (barcodePrefix.prefix == prefix) { - return barcodePrefix - } - } - return UNKNOWN - } - } -} -//1001000000 \ No newline at end of file diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/BarcodeReadDelegate.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/BarcodeReadDelegate.kt index 50ff0ca..1b500d9 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/BarcodeReadDelegate.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/util/BarcodeReadDelegate.kt @@ -1,6 +1,7 @@ package info.nukoneko.cuc.android.kidspos.util import android.view.KeyEvent +import com.orhanobut.logger.Logger import info.nukoneko.cuc.android.kidspos.error.IllegalBarcodeException class BarcodeReadDelegate(var listener: OnBarcodeReadListener?) { @@ -13,13 +14,26 @@ class BarcodeReadDelegate(var listener: OnBarcodeReadListener?) { } if (event.keyCode == KeyEvent.KEYCODE_ENTER) { if (readingValue.isNotEmpty() && readingValue.length >= 5) { + Logger.d("Barcode read: raw=$readingValue") + + // 両脇が 5222 だったらそれぞれ A に変換する + var processedValue = readingValue + if (processedValue.startsWith("5222") && processedValue.endsWith("5222")) { + processedValue = + "A" + processedValue.substring(4, processedValue.length - 4) + "A" + Logger.d("Barcode converted: $readingValue -> $processedValue") + } + // 10桁なら返す - when (readingValue.length) { + when (processedValue.length) { 10 -> { - val prefix = readingValue.substring(2, 4) - listener?.onReadSuccess(readingValue, BarcodeKind.prefixOf(prefix)) + listener?.onReadSuccess(processedValue) + } + + else -> { + Logger.e("Barcode failed: invalid length (${processedValue.length}): $processedValue") + listener?.onReadFailed(IllegalBarcodeException(processedValue)) } - else -> listener?.onReadFailed(IllegalBarcodeException(readingValue)) } } readingValue = "" @@ -33,7 +47,7 @@ class BarcodeReadDelegate(var listener: OnBarcodeReadListener?) { } interface OnBarcodeReadListener { - fun onReadSuccess(barcode: String, kind: BarcodeKind) + fun onReadSuccess(barcode: String) fun onReadFailed(e: IllegalBarcodeException) } diff --git a/app/src/main/res/layout/fragment_item_list.xml b/app/src/main/res/layout/fragment_item_list.xml index 6dcdf66..70b6c9f 100644 --- a/app/src/main/res/layout/fragment_item_list.xml +++ b/app/src/main/res/layout/fragment_item_list.xml @@ -64,22 +64,10 @@ android:textSize="22sp" android:visibility="@{viewModel.addItemButtonVisibility}" /> - - - - + android:layout_weight="1" />